Clone of https://github.com/NixOS/nixpkgs.git (to stress-test knotserver)
1From 0fd815b7cae40478f7d34c6003be7525b2ca2687 Mon Sep 17 00:00:00 2001
2From: renesat <self@renesat.me>
3Date: Sat, 12 Jul 2025 02:31:35 +0200
4Subject: [PATCH] update datalad buildsupport scrypts
5
6---
7 _datalad_buildsupport/formatters.py | 18 ++-
8 _datalad_buildsupport/setup.py | 227 +++++++++++++++++++++++-----
9 2 files changed, 200 insertions(+), 45 deletions(-)
10
11diff --git a/_datalad_buildsupport/formatters.py b/_datalad_buildsupport/formatters.py
12index 5ac01de..fb21875 100644
13--- a/_datalad_buildsupport/formatters.py
14+++ b/_datalad_buildsupport/formatters.py
15@@ -7,7 +7,10 @@
16
17 import argparse
18 import datetime
19+import os
20 import re
21+import time
22+from textwrap import wrap
23
24
25 class ManPageFormatter(argparse.HelpFormatter):
26@@ -24,7 +27,7 @@ def __init__(self,
27 authors=None,
28 version=None
29 ):
30-
31+ from datalad import cfg
32 super(ManPageFormatter, self).__init__(
33 prog,
34 indent_increment=indent_increment,
35@@ -33,7 +36,10 @@ def __init__(self,
36
37 self._prog = prog
38 self._section = 1
39- self._today = datetime.date.today().strftime('%Y\\-%m\\-%d')
40+ self._today = datetime.datetime.fromtimestamp(
41+ cfg.obtain('datalad.source.epoch'),
42+ datetime.timezone.utc
43+ ).strftime('%Y\\-%m\\-%d')
44 self._ext_sections = ext_sections
45 self._version = version
46
47@@ -75,7 +81,7 @@ def _mk_title(self, prog):
48
49 def _mk_name(self, prog, desc):
50 """
51- this method is in consitent with others ... it relies on
52+ this method is in consistent with others ... it relies on
53 distribution
54 """
55 desc = desc.splitlines()[0] if desc else 'it is in the name'
56@@ -195,7 +201,9 @@ def _mk_synopsis(self, parser):
57 parser._mutually_exclusive_groups, '')
58
59 usage = usage.replace('%s ' % self._prog, '')
60- usage = 'Synopsis\n--------\n::\n\n %s %s\n' \
61+ usage = '\n'.join(wrap(
62+ usage, break_on_hyphens=False, subsequent_indent=6*' '))
63+ usage = 'Synopsis\n--------\n::\n\n %s %s\n\n' \
64 % (self._markup(self._prog), usage)
65 return usage
66
67@@ -251,7 +259,7 @@ def _mk_options(self, parser):
68
69 def _format_action(self, action):
70 # determine the required width and the entry label
71- action_header = self._format_action_invocation(action)
72+ action_header = self._format_action_invocation(action, doubledash='-\\-')
73
74 if action.help:
75 help_text = self._expand_help(action)
76diff --git a/_datalad_buildsupport/setup.py b/_datalad_buildsupport/setup.py
77index 27e0821..e3ba793 100644
78--- a/_datalad_buildsupport/setup.py
79+++ b/_datalad_buildsupport/setup.py
80@@ -8,19 +8,51 @@
81
82 import datetime
83 import os
84-
85-from os.path import (
86- dirname,
87- join as opj,
88+import platform
89+import sys
90+from os import (
91+ linesep,
92+ makedirs,
93 )
94-from setuptools import Command, DistutilsOptionError
95-from setuptools.config import read_configuration
96-
97-import versioneer
98+from os.path import dirname
99+from os.path import join as opj
100+from os.path import sep as pathsep
101+from os.path import splitext
102+
103+import setuptools
104+from genericpath import exists
105+from packaging.version import Version
106+from setuptools import (
107+ Command,
108+ find_namespace_packages,
109+ findall,
110+ setup,
111+)
112+from setuptools.errors import OptionError
113
114 from . import formatters as fmt
115
116
117+def _path_rel2file(*p):
118+ # dirname instead of joining with pardir so it works if
119+ # datalad_build_support/ is just symlinked into some extension
120+ # while developing
121+ return opj(dirname(dirname(__file__)), *p)
122+
123+
124+def get_version(name):
125+ """Determine version via importlib_metadata
126+
127+ Parameters
128+ ----------
129+ name: str
130+ Name of the folder (package) where from to read version.py
131+ """
132+ # delay import so we do not require it for a simple setup stage
133+ from importlib.metadata import version as importlib_version
134+ return importlib_version(name)
135+
136+
137 class BuildManPage(Command):
138 # The BuildManPage code was originally distributed
139 # under the same License of Python
140@@ -29,33 +61,27 @@ class BuildManPage(Command):
141 description = 'Generate man page from an ArgumentParser instance.'
142
143 user_options = [
144- ('manpath=', None,
145- 'output path for manpages (relative paths are relative to the '
146- 'datalad package)'),
147- ('rstpath=', None,
148- 'output path for RST files (relative paths are relative to the '
149- 'datalad package)'),
150+ ('manpath=', None, 'output path for manpages'),
151+ ('rstpath=', None, 'output path for RST files'),
152 ('parser=', None, 'module path to an ArgumentParser instance'
153 '(e.g. mymod:func, where func is a method or function which return'
154 'a dict with one or more arparse.ArgumentParser instances.'),
155- ('cmdsuite=', None, 'module path to an extension command suite '
156- '(e.g. mymod:command_suite) to limit the build to the contained '
157- 'commands.'),
158 ]
159
160 def initialize_options(self):
161 self.manpath = opj('build', 'man')
162 self.rstpath = opj('docs', 'source', 'generated', 'man')
163- self.parser = 'datalad.cmdline.main:setup_parser'
164- self.cmdsuite = None
165+ self.parser = 'datalad.cli.parser:setup_parser'
166
167 def finalize_options(self):
168 if self.manpath is None:
169- raise DistutilsOptionError('\'manpath\' option is required')
170+ raise OptionError('\'manpath\' option is required')
171 if self.rstpath is None:
172- raise DistutilsOptionError('\'rstpath\' option is required')
173+ raise OptionError('\'rstpath\' option is required')
174 if self.parser is None:
175- raise DistutilsOptionError('\'parser\' option is required')
176+ raise OptionError('\'parser\' option is required')
177+ self.manpath = _path_rel2file(self.manpath)
178+ self.rstpath = _path_rel2file(self.rstpath)
179 mod_name, func_name = self.parser.split(':')
180 fromlist = mod_name.split('.')
181 try:
182@@ -64,18 +90,10 @@ def finalize_options(self):
183 ['datalad'],
184 formatter_class=fmt.ManPageFormatter,
185 return_subparsers=True,
186- # ignore extensions only for the main package to avoid pollution
187- # with all extension commands that happen to be installed
188- help_ignore_extensions=self.distribution.get_name() == 'datalad')
189+ help_ignore_extensions=True)
190
191 except ImportError as err:
192 raise err
193- if self.cmdsuite:
194- mod_name, suite_name = self.cmdsuite.split(':')
195- mod = __import__(mod_name, fromlist=mod_name.split('.'))
196- suite = getattr(mod, suite_name)
197- self.cmdlist = [c[2] if len(c) > 2 else c[1].replace('_', '-').lower()
198- for c in suite[1]]
199
200 self.announce('Writing man page(s) to %s' % self.manpath)
201 self._today = datetime.date.today()
202@@ -125,12 +143,9 @@ def run(self):
203 #appname = self._parser.prog
204 appname = 'datalad'
205
206- cfg = read_configuration(
207- opj(dirname(dirname(__file__)), 'setup.cfg'))['metadata']
208-
209 sections = {
210 'Authors': """{0} is developed by {1} <{2}>.""".format(
211- appname, cfg['author'], cfg['author_email']),
212+ appname, dist.get_author(), dist.get_author_email()),
213 }
214
215 for cls, opath, ext in ((fmt.ManPageFormatter, self.manpath, '1'),
216@@ -138,8 +153,6 @@ def run(self):
217 if not os.path.exists(opath):
218 os.makedirs(opath)
219 for cmdname in getattr(self, 'cmdline_names', list(self._parser)):
220- if hasattr(self, 'cmdlist') and cmdname not in self.cmdlist:
221- continue
222 p = self._parser[cmdname]
223 cmdname = "{0}{1}".format(
224 'datalad ' if cmdname != 'datalad' else '',
225@@ -147,7 +160,7 @@ def run(self):
226 format = cls(
227 cmdname,
228 ext_sections=sections,
229- version=versioneer.get_version())
230+ version=get_version(getattr(self, 'mod_name', appname)))
231 formatted = format.format_man_page(p)
232 with open(opj(opath, '{0}.{1}'.format(
233 cmdname.replace(' ', '-'),
234@@ -156,6 +169,42 @@ def run(self):
235 f.write(formatted)
236
237
238+class BuildRSTExamplesFromScripts(Command):
239+ description = 'Generate RST variants of example shell scripts.'
240+
241+ user_options = [
242+ ('expath=', None, 'path to look for example scripts'),
243+ ('rstpath=', None, 'output path for RST files'),
244+ ]
245+
246+ def initialize_options(self):
247+ self.expath = opj('docs', 'examples')
248+ self.rstpath = opj('docs', 'source', 'generated', 'examples')
249+
250+ def finalize_options(self):
251+ if self.expath is None:
252+ raise OptionError('\'expath\' option is required')
253+ if self.rstpath is None:
254+ raise OptionError('\'rstpath\' option is required')
255+ self.expath = _path_rel2file(self.expath)
256+ self.rstpath = _path_rel2file(self.rstpath)
257+ self.announce('Converting example scripts')
258+
259+ def run(self):
260+ opath = self.rstpath
261+ if not os.path.exists(opath):
262+ os.makedirs(opath)
263+
264+ from glob import glob
265+ for example in glob(opj(self.expath, '*.sh')):
266+ exname = os.path.basename(example)[:-3]
267+ with open(opj(opath, '{0}.rst'.format(exname)), 'w') as out:
268+ fmt.cmdline_example_to_rst(
269+ open(example),
270+ out=out,
271+ ref='_example_{0}'.format(exname))
272+
273+
274 class BuildConfigInfo(Command):
275 description = 'Generate RST documentation for all config items.'
276
277@@ -168,7 +217,8 @@ def initialize_options(self):
278
279 def finalize_options(self):
280 if self.rstpath is None:
281- raise DistutilsOptionError('\'rstpath\' option is required')
282+ raise OptionError('\'rstpath\' option is required')
283+ self.rstpath = _path_rel2file(self.rstpath)
284 self.announce('Generating configuration documentation')
285
286 def run(self):
287@@ -176,8 +226,8 @@ def run(self):
288 if not os.path.exists(opath):
289 os.makedirs(opath)
290
291- from datalad.interface.common_cfg import definitions as cfgdefs
292 from datalad.dochelpers import _indent
293+ from datalad.interface.common_cfg import definitions as cfgdefs
294
295 categories = {
296 'global': {},
297@@ -218,3 +268,100 @@ def run(self):
298 desc_tmpl += 'undocumented\n'
299 v.update(docs)
300 rst.write(_indent(desc_tmpl.format(**v), ' '))
301+
302+
303+def get_long_description_from_README():
304+ """Read README.md, convert to .rst using pypandoc
305+
306+ If pypandoc is not available or fails - just output original .md.
307+
308+ Returns
309+ -------
310+ dict
311+ with keys long_description and possibly long_description_content_type
312+ for newer setuptools which support uploading of markdown as is.
313+ """
314+ # PyPI used to not render markdown. Workaround for a sane appearance
315+ # https://github.com/pypa/pypi-legacy/issues/148#issuecomment-227757822
316+ # is still in place for older setuptools
317+
318+ README = opj(_path_rel2file('README.md'))
319+
320+ ret = {}
321+ if Version(setuptools.__version__) >= Version('38.6.0'):
322+ # check than this
323+ ret['long_description'] = open(README).read()
324+ ret['long_description_content_type'] = 'text/markdown'
325+ return ret
326+
327+ # Convert or fall-back
328+ try:
329+ import pypandoc
330+ return {'long_description': pypandoc.convert(README, 'rst')}
331+ except (ImportError, OSError) as exc:
332+ # attempting to install pandoc via brew on OSX currently hangs and
333+ # pypandoc imports but throws OSError demanding pandoc
334+ print(
335+ "WARNING: pypandoc failed to import or thrown an error while "
336+ "converting"
337+ " README.md to RST: %r .md version will be used as is" % exc
338+ )
339+ return {'long_description': open(README).read()}
340+
341+
342+def findsome(subdir, extensions):
343+ """Find files under subdir having specified extensions
344+
345+ Leading directory (datalad) gets stripped
346+ """
347+ return [
348+ f.split(pathsep, 1)[1] for f in findall(opj('datalad', subdir))
349+ if splitext(f)[-1].lstrip('.') in extensions
350+ ]
351+
352+
353+def datalad_setup(name, **kwargs):
354+ """A helper for a typical invocation of setuptools.setup.
355+
356+ If not provided in kwargs, following fields will be autoset to the defaults
357+ or obtained from the present on the file system files:
358+
359+ - author
360+ - author_email
361+ - packages -- all found packages which start with `name`
362+ - long_description -- converted to .rst using pypandoc README.md
363+ - version -- parsed `__version__` within `name/version.py`
364+
365+ Parameters
366+ ----------
367+ name: str
368+ Name of the Python package
369+ **kwargs:
370+ The rest of the keyword arguments passed to setuptools.setup as is
371+ """
372+ # Simple defaults
373+ for k, v in {
374+ 'author': "The DataLad Team and Contributors",
375+ 'author_email': "team@datalad.org"
376+ }.items():
377+ if kwargs.get(k) is None:
378+ kwargs[k] = v
379+
380+ # More complex, requiring some function call
381+
382+ # Only recentish versions of find_packages support include
383+ # packages = find_packages('.', include=['datalad*'])
384+ # so we will filter manually for maximal compatibility
385+ if kwargs.get('packages') is None:
386+ # Use find_namespace_packages() in order to include folders that
387+ # contain data files but no Python code
388+ kwargs['packages'] = [pkg for pkg in find_namespace_packages('.') if pkg.startswith(name)]
389+ if kwargs.get('long_description') is None:
390+ kwargs.update(get_long_description_from_README())
391+
392+ cmdclass = kwargs.get('cmdclass', {})
393+ # Check if command needs some module specific handling
394+ for v in cmdclass.values():
395+ if hasattr(v, 'handle_module'):
396+ getattr(v, 'handle_module')(name, **kwargs)
397+ return setup(name=name, **kwargs)
398