summaryrefslogtreecommitdiff
path: root/pkgs/development/python-modules/datalad-gooey/setuptools.patch
blob: 0f286878fcb800dfa87268d3f1a07fcbd9e356e0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
From 0fd815b7cae40478f7d34c6003be7525b2ca2687 Mon Sep 17 00:00:00 2001
From: renesat <self@renesat.me>
Date: Sat, 12 Jul 2025 02:31:35 +0200
Subject: [PATCH] update datalad buildsupport scrypts

---
 _datalad_buildsupport/formatters.py |  18 ++-
 _datalad_buildsupport/setup.py      | 227 +++++++++++++++++++++++-----
 2 files changed, 200 insertions(+), 45 deletions(-)

diff --git a/_datalad_buildsupport/formatters.py b/_datalad_buildsupport/formatters.py
index 5ac01de..fb21875 100644
--- a/_datalad_buildsupport/formatters.py
+++ b/_datalad_buildsupport/formatters.py
@@ -7,7 +7,10 @@
 
 import argparse
 import datetime
+import os
 import re
+import time
+from textwrap import wrap
 
 
 class ManPageFormatter(argparse.HelpFormatter):
@@ -24,7 +27,7 @@ def __init__(self,
                  authors=None,
                  version=None
                  ):
-
+        from datalad import cfg
         super(ManPageFormatter, self).__init__(
             prog,
             indent_increment=indent_increment,
@@ -33,7 +36,10 @@ def __init__(self,
 
         self._prog = prog
         self._section = 1
-        self._today = datetime.date.today().strftime('%Y\\-%m\\-%d')
+        self._today = datetime.datetime.fromtimestamp(
+            cfg.obtain('datalad.source.epoch'),
+            datetime.timezone.utc
+        ).strftime('%Y\\-%m\\-%d')
         self._ext_sections = ext_sections
         self._version = version
 
@@ -75,7 +81,7 @@ def _mk_title(self, prog):
 
     def _mk_name(self, prog, desc):
         """
-        this method is in consitent with others ... it relies on
+        this method is in consistent with others ... it relies on
         distribution
         """
         desc = desc.splitlines()[0] if desc else 'it is in the name'
@@ -195,7 +201,9 @@ def _mk_synopsis(self, parser):
                                    parser._mutually_exclusive_groups, '')
 
         usage = usage.replace('%s ' % self._prog, '')
-        usage = 'Synopsis\n--------\n::\n\n  %s %s\n' \
+        usage = '\n'.join(wrap(
+            usage, break_on_hyphens=False, subsequent_indent=6*' '))
+        usage = 'Synopsis\n--------\n::\n\n  %s %s\n\n' \
                 % (self._markup(self._prog), usage)
         return usage
 
@@ -251,7 +259,7 @@ def _mk_options(self, parser):
 
     def _format_action(self, action):
         # determine the required width and the entry label
-        action_header = self._format_action_invocation(action)
+        action_header = self._format_action_invocation(action, doubledash='-\\-')
 
         if action.help:
             help_text = self._expand_help(action)
diff --git a/_datalad_buildsupport/setup.py b/_datalad_buildsupport/setup.py
index 27e0821..e3ba793 100644
--- a/_datalad_buildsupport/setup.py
+++ b/_datalad_buildsupport/setup.py
@@ -8,19 +8,51 @@
 
 import datetime
 import os
-
-from os.path import (
-    dirname,
-    join as opj,
+import platform
+import sys
+from os import (
+    linesep,
+    makedirs,
 )
-from setuptools import Command, DistutilsOptionError
-from setuptools.config import read_configuration
-
-import versioneer
+from os.path import dirname
+from os.path import join as opj
+from os.path import sep as pathsep
+from os.path import splitext
+
+import setuptools
+from genericpath import exists
+from packaging.version import Version
+from setuptools import (
+    Command,
+    find_namespace_packages,
+    findall,
+    setup,
+)
+from setuptools.errors import OptionError
 
 from . import formatters as fmt
 
 
+def _path_rel2file(*p):
+    # dirname instead of joining with pardir so it works if
+    # datalad_build_support/ is just symlinked into some extension
+    # while developing
+    return opj(dirname(dirname(__file__)), *p)
+
+
+def get_version(name):
+    """Determine version via importlib_metadata
+
+    Parameters
+    ----------
+    name: str
+      Name of the folder (package) where from to read version.py
+    """
+    # delay import so we do not require it for a simple setup stage
+    from importlib.metadata import version as importlib_version
+    return importlib_version(name)
+
+
 class BuildManPage(Command):
     # The BuildManPage code was originally distributed
     # under the same License of Python
@@ -29,33 +61,27 @@ class BuildManPage(Command):
     description = 'Generate man page from an ArgumentParser instance.'
 
     user_options = [
-        ('manpath=', None,
-         'output path for manpages (relative paths are relative to the '
-         'datalad package)'),
-        ('rstpath=', None,
-         'output path for RST files (relative paths are relative to the '
-         'datalad package)'),
+        ('manpath=', None, 'output path for manpages'),
+        ('rstpath=', None, 'output path for RST files'),
         ('parser=', None, 'module path to an ArgumentParser instance'
          '(e.g. mymod:func, where func is a method or function which return'
          'a dict with one or more arparse.ArgumentParser instances.'),
-        ('cmdsuite=', None, 'module path to an extension command suite '
-         '(e.g. mymod:command_suite) to limit the build to the contained '
-         'commands.'),
     ]
 
     def initialize_options(self):
         self.manpath = opj('build', 'man')
         self.rstpath = opj('docs', 'source', 'generated', 'man')
-        self.parser = 'datalad.cmdline.main:setup_parser'
-        self.cmdsuite = None
+        self.parser = 'datalad.cli.parser:setup_parser'
 
     def finalize_options(self):
         if self.manpath is None:
-            raise DistutilsOptionError('\'manpath\' option is required')
+            raise OptionError('\'manpath\' option is required')
         if self.rstpath is None:
-            raise DistutilsOptionError('\'rstpath\' option is required')
+            raise OptionError('\'rstpath\' option is required')
         if self.parser is None:
-            raise DistutilsOptionError('\'parser\' option is required')
+            raise OptionError('\'parser\' option is required')
+        self.manpath = _path_rel2file(self.manpath)
+        self.rstpath = _path_rel2file(self.rstpath)
         mod_name, func_name = self.parser.split(':')
         fromlist = mod_name.split('.')
         try:
@@ -64,18 +90,10 @@ def finalize_options(self):
                 ['datalad'],
                 formatter_class=fmt.ManPageFormatter,
                 return_subparsers=True,
-                # ignore extensions only for the main package to avoid pollution
-                # with all extension commands that happen to be installed
-                help_ignore_extensions=self.distribution.get_name() == 'datalad')
+                help_ignore_extensions=True)
 
         except ImportError as err:
             raise err
-        if self.cmdsuite:
-            mod_name, suite_name = self.cmdsuite.split(':')
-            mod = __import__(mod_name, fromlist=mod_name.split('.'))
-            suite = getattr(mod, suite_name)
-            self.cmdlist = [c[2] if len(c) > 2 else c[1].replace('_', '-').lower()
-                            for c in suite[1]]
 
         self.announce('Writing man page(s) to %s' % self.manpath)
         self._today = datetime.date.today()
@@ -125,12 +143,9 @@ def run(self):
         #appname = self._parser.prog
         appname = 'datalad'
 
-        cfg = read_configuration(
-            opj(dirname(dirname(__file__)), 'setup.cfg'))['metadata']
-
         sections = {
             'Authors': """{0} is developed by {1} <{2}>.""".format(
-                appname, cfg['author'], cfg['author_email']),
+                appname, dist.get_author(), dist.get_author_email()),
         }
 
         for cls, opath, ext in ((fmt.ManPageFormatter, self.manpath, '1'),
@@ -138,8 +153,6 @@ def run(self):
             if not os.path.exists(opath):
                 os.makedirs(opath)
             for cmdname in getattr(self, 'cmdline_names', list(self._parser)):
-                if hasattr(self, 'cmdlist') and cmdname not in self.cmdlist:
-                    continue
                 p = self._parser[cmdname]
                 cmdname = "{0}{1}".format(
                     'datalad ' if cmdname != 'datalad' else '',
@@ -147,7 +160,7 @@ def run(self):
                 format = cls(
                     cmdname,
                     ext_sections=sections,
-                    version=versioneer.get_version())
+                    version=get_version(getattr(self, 'mod_name', appname)))
                 formatted = format.format_man_page(p)
                 with open(opj(opath, '{0}.{1}'.format(
                         cmdname.replace(' ', '-'),
@@ -156,6 +169,42 @@ def run(self):
                     f.write(formatted)
 
 
+class BuildRSTExamplesFromScripts(Command):
+    description = 'Generate RST variants of example shell scripts.'
+
+    user_options = [
+        ('expath=', None, 'path to look for example scripts'),
+        ('rstpath=', None, 'output path for RST files'),
+    ]
+
+    def initialize_options(self):
+        self.expath = opj('docs', 'examples')
+        self.rstpath = opj('docs', 'source', 'generated', 'examples')
+
+    def finalize_options(self):
+        if self.expath is None:
+            raise OptionError('\'expath\' option is required')
+        if self.rstpath is None:
+            raise OptionError('\'rstpath\' option is required')
+        self.expath = _path_rel2file(self.expath)
+        self.rstpath = _path_rel2file(self.rstpath)
+        self.announce('Converting example scripts')
+
+    def run(self):
+        opath = self.rstpath
+        if not os.path.exists(opath):
+            os.makedirs(opath)
+
+        from glob import glob
+        for example in glob(opj(self.expath, '*.sh')):
+            exname = os.path.basename(example)[:-3]
+            with open(opj(opath, '{0}.rst'.format(exname)), 'w') as out:
+                fmt.cmdline_example_to_rst(
+                    open(example),
+                    out=out,
+                    ref='_example_{0}'.format(exname))
+
+
 class BuildConfigInfo(Command):
     description = 'Generate RST documentation for all config items.'
 
@@ -168,7 +217,8 @@ def initialize_options(self):
 
     def finalize_options(self):
         if self.rstpath is None:
-            raise DistutilsOptionError('\'rstpath\' option is required')
+            raise OptionError('\'rstpath\' option is required')
+        self.rstpath = _path_rel2file(self.rstpath)
         self.announce('Generating configuration documentation')
 
     def run(self):
@@ -176,8 +226,8 @@ def run(self):
         if not os.path.exists(opath):
             os.makedirs(opath)
 
-        from datalad.interface.common_cfg import definitions as cfgdefs
         from datalad.dochelpers import _indent
+        from datalad.interface.common_cfg import definitions as cfgdefs
 
         categories = {
             'global': {},
@@ -218,3 +268,100 @@ def run(self):
                         desc_tmpl += 'undocumented\n'
                     v.update(docs)
                     rst.write(_indent(desc_tmpl.format(**v), '    '))
+
+
+def get_long_description_from_README():
+    """Read README.md, convert to .rst using pypandoc
+
+    If pypandoc is not available or fails - just output original .md.
+
+    Returns
+    -------
+    dict
+      with keys long_description and possibly long_description_content_type
+      for newer setuptools which support uploading of markdown as is.
+    """
+    # PyPI used to not render markdown. Workaround for a sane appearance
+    # https://github.com/pypa/pypi-legacy/issues/148#issuecomment-227757822
+    # is still in place for older setuptools
+
+    README = opj(_path_rel2file('README.md'))
+
+    ret = {}
+    if Version(setuptools.__version__) >= Version('38.6.0'):
+        # check than this
+        ret['long_description'] = open(README).read()
+        ret['long_description_content_type'] = 'text/markdown'
+        return ret
+
+    # Convert or fall-back
+    try:
+        import pypandoc
+        return {'long_description': pypandoc.convert(README, 'rst')}
+    except (ImportError, OSError) as exc:
+        # attempting to install pandoc via brew on OSX currently hangs and
+        # pypandoc imports but throws OSError demanding pandoc
+        print(
+                "WARNING: pypandoc failed to import or thrown an error while "
+                "converting"
+                " README.md to RST: %r   .md version will be used as is" % exc
+        )
+        return {'long_description': open(README).read()}
+
+
+def findsome(subdir, extensions):
+    """Find files under subdir having specified extensions
+
+    Leading directory (datalad) gets stripped
+    """
+    return [
+        f.split(pathsep, 1)[1] for f in findall(opj('datalad', subdir))
+        if splitext(f)[-1].lstrip('.') in extensions
+    ]
+
+
+def datalad_setup(name, **kwargs):
+    """A helper for a typical invocation of setuptools.setup.
+
+    If not provided in kwargs, following fields will be autoset to the defaults
+    or obtained from the present on the file system files:
+
+    - author
+    - author_email
+    - packages -- all found packages which start with `name`
+    - long_description -- converted to .rst using pypandoc README.md
+    - version -- parsed `__version__` within `name/version.py`
+
+    Parameters
+    ----------
+    name: str
+        Name of the Python package
+    **kwargs:
+        The rest of the keyword arguments passed to setuptools.setup as is
+    """
+    # Simple defaults
+    for k, v in {
+        'author': "The DataLad Team and Contributors",
+        'author_email': "team@datalad.org"
+    }.items():
+        if kwargs.get(k) is None:
+            kwargs[k] = v
+
+    # More complex, requiring some function call
+
+    # Only recentish versions of find_packages support include
+    # packages = find_packages('.', include=['datalad*'])
+    # so we will filter manually for maximal compatibility
+    if kwargs.get('packages') is None:
+        # Use find_namespace_packages() in order to include folders that
+        # contain data files but no Python code
+        kwargs['packages'] = [pkg for pkg in find_namespace_packages('.') if pkg.startswith(name)]
+    if kwargs.get('long_description') is None:
+        kwargs.update(get_long_description_from_README())
+
+    cmdclass = kwargs.get('cmdclass', {})
+    # Check if command needs some module specific handling
+    for v in cmdclass.values():
+        if hasattr(v, 'handle_module'):
+            getattr(v, 'handle_module')(name, **kwargs)
+    return setup(name=name, **kwargs)