Clone of https://github.com/NixOS/nixpkgs.git (to stress-test knotserver)
at haskell-updates 398 lines 15 kB view raw
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