Browse Source

move setuptools render logic into shut

shut-new-model
Niklas Rosenstein 9 months ago
parent
commit
1b79bfe565
No known key found for this signature in database GPG Key ID: 6D269B33D25F6C6
13 changed files with 566 additions and 40 deletions
  1. + 1
    - 1
      package.yaml
  2. + 24
    - 12
      setup.py
  3. + 5
    - 0
      src/shut/checks/__init__.py
  4. + 3
    - 3
      src/shut/checks/basic.py
  5. + 19
    - 16
      src/shut/checks/core.py
  6. + 9
    - 2
      src/shut/commands/commons/new.py
  7. + 1
    - 0
      src/shut/commands/pkg/__init__.py
  8. + 9
    - 3
      src/shut/commands/pkg/update.py
  9. + 34
    - 3
      src/shut/model/package.py
  10. + 32
    - 0
      src/shut/update/__init__.py
  11. + 56
    - 0
      src/shut/update/core.py
  12. + 333
    - 0
      src/shut/update/setuptools.py
  13. + 40
    - 0
      src/shut/utils/type_registry.py

+ 1
- 1
package.yaml

@ -7,7 +7,7 @@ package:
author: Niklas Rosenstein <rosensteinniklas@gmail.com>
requirements:
- python ^2.7|^3.4
- python ^3.6
- beautifulsoup4 ^4.8.1
- click ^7.0
- jinja2 ^2.11.1

+ 24
- 12
setup.py

@ -1,5 +1,5 @@
# This file was automatically generated by Shore. Do not edit manually.
# For more information on Shore see https://pypi.org/project/nr.shore/
# This file was auto-generated by Shut. DO NOT EDIT
# For more information about Shut, check out https://pypi.org/project/shut/
from __future__ import print_function
import io
@ -8,7 +8,7 @@ import re
import setuptools
import sys
with io.open('src/shore/__init__.py', encoding='utf8') as fp:
with io.open('src/shut/__init__.py', encoding='utf8') as fp:
version = re.search(r"__version__\s*=\s*'(.*)'", fp.read()).group(1)
readme_file = 'README.md'
@ -19,10 +19,27 @@ else:
print("warning: file \"{}\" does not exist.".format(readme_file), file=sys.stderr)
long_description = None
requirements = ['beautifulsoup4 >=4.8.1,<5.0.0', 'click >=7.0,<8.0.0', 'jinja2 >=2.11.1,<3.0.0', 'networkx >=2.4,<3.0.0', 'nr.databind.core >=0.0.20,<0.1.0', 'nr.databind.json >=0.0.13,<0.1.0', 'nr.fs >=1.5.0,<2.0.0', 'nr.pylang.utils >=0.0.1,<1.0.0', 'nr.proxy >=0.0.1,<1.0.0', 'nr.utils.git >=0.1.3,<0.2.0', 'requests >=2.22.0,<3.0.0', 'packaging >=20.1,<21.0.0', 'PyYAML >=5.1.0,<6.0.0', 'termcolor >=1.1.0,<2.0.0', 'twine', 'wheel']
requirements = [
'beautifulsoup4 >=4.8.1,<5.0.0',
'click >=7.0,<8.0.0',
'jinja2 >=2.11.1,<3.0.0',
'networkx >=2.4,<3.0.0',
'nr.databind.core >=0.0.21,<0.1.0',
'nr.databind.json >=0.0.13,<0.1.0',
'nr.fs >=1.5.0,<2.0.0',
'nr.pylang.utils >=0.0.1,<1.0.0',
'nr.proxy >=0.0.1,<1.0.0',
'nr.utils.git >=0.1.3,<0.2.0',
'requests >=2.22.0,<3.0.0',
'packaging >=20.1,<21.0.0',
'PyYAML >=5.1.0,<6.0.0',
'termcolor >=1.1.0,<2.0.0',
'twine',
'wheel',
]
setuptools.setup(
name = 'nr.shore',
name = 'shut',
version = version,
author = 'Niklas Rosenstein',
author_email = 'rosensteinniklas@gmail.com',
@ -31,13 +48,13 @@ setuptools.setup(
long_description_content_type = 'text/markdown',
url = 'https://git.niklasrosenstein.com/NiklasRosenstein/shore',
license = 'MIT',
packages = setuptools.find_packages('src', ['test', 'test.*', 'docs', 'docs.*']),
packages = setuptools.find_packages('src', ['test', 'test.*', 'tests', 'tests.*', 'docs', 'docs.*']),
package_dir = {'': 'src'},
include_package_data = True,
install_requires = requirements,
extras_require = {},
tests_require = [],
python_requires = None, # TODO: '>=2.7,<3.0.0|>=3.4,<4.0.0',
python_requires = '>=3.6,<4.0.0',
data_files = [],
entry_points = {
'console_scripts': [
@ -56,9 +73,4 @@ setuptools.setup(
cmdclass = {},
keywords = [],
classifiers = [],
options = {
'bdist_wheel': {
'universal': True,
},
},
)

+ 5
- 0
src/shut/checks/__init__.py

@ -19,6 +19,11 @@
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
"""
The `shut.checks` package implements all the sanity checks that are run over a monoreop and
package definition to prevent common pitfalls and errors.
"""
from .core import (
CheckStatus,
CheckResult,

+ 3
- 3
src/shut/checks/basic.py

@ -20,7 +20,7 @@
# IN THE SOFTWARE.
from .core import CheckResult, CheckStatus, Checker, check, register_checker
from shut.model import MonorepoModel, PackageModel, Project
from shut.model import AbstractProjectModel, Project
from typing import Iterable, Optional, Union
@ -37,5 +37,5 @@ class BasicChecker(Checker):
', '.join(map(str, obj.unknown_keys)) if obj.unknown_keys else None)
register_checker(MonorepoModel, BasicChecker)
register_checker(PackageModel, BasicChecker)
register_checker(AbstractProjectModel, BasicChecker)

+ 19
- 16
src/shut/checks/core.py

@ -28,6 +28,17 @@ from typing import Callable, Iterable, Generic, Type, TypeVar, Union
import enum
import types
from shut.model import Project
from shut.utils.type_registry import TypeRegistry
__all__ = [
'CheckStatus',
'CheckResult',
'Check',
'register_checker',
'get_checks',
]
T = TypeVar('T')
@ -56,7 +67,7 @@ def check(name: str) -> Callable[[Callable], Callable]:
class Checker(Generic[T]):
def get_checks(self, project: >'Project', subject: T) -> Iterable[Check]:
def get_checks(self, project: Project, subject: T) -> Iterable[Check]:
"""
Yield #Check objects for the *subject*. By default, all methods decorated with
#check() are called.
@ -75,30 +86,22 @@ class Checker(Generic[T]):
if index is None:
yield Check(value.__check_name__, CheckResult(CheckStatus.PASSED, None))
registry = {}
registry = TypeRegistry[Type[Checker]]()
def register_checker(t: Type[T], checker: Type[Checker[T]]) -> Type[Checker]:
def register_checker(t: Type[T], checker: Type[Checker[T]]) -> None:
"""
Decorator to register a #Checker subclass.
Register a *checker* class to run checks for type *t*.
"""
registry.setdefault(t, []).append(checker)
registry.put(t, checker)
def get_checks(project: >'Project', obj: T) -> Iterable[Check]:
def get_checks(project: Project, obj: T) -> Iterable[Check]:
"""
Returns all checks from the checkers registered for the type of *obj*.
"""
for checker in registry.get(type(obj), []):
for checker in registry.for_type(type(obj)):
yield from checker().get_checks(project, obj)
__all__ = [
'CheckStatus',
'CheckResult',
'Check',
'register_checker',
'get_checks',
]

+ 9
- 2
src/shut/commands/commons/new.py

@ -19,10 +19,12 @@
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
import os
import subprocess
from typing import Optional
import jinja2
import nr.fs
from shut.model.author import Author
from shut.utils.external.license import get_license_metadata, wrap_license_text
from shut.utils.io.virtual import VirtualFiles
@ -75,10 +77,15 @@ def render_template(fp, template_string, template_vars):
def write_files(files: VirtualFiles, target_directory: str, force: bool = False, dry: bool = False):
def _rel(fn: str) -> str:
path = os.path.relpath(fn)
if nr.fs.issub(path):
return path
return fn
files.write_all(
target_directory,
on_write=lambda fn: print(colored('Write ' + fn, 'cyan')),
on_skip=lambda fn: print(colored('Skip ' + fn, 'yellow')),
on_write=lambda fn: print(colored('Write ' + _rel(fn), 'cyan')),
on_skip=lambda fn: print(colored('Skip ' + _rel(fn), 'yellow')),
overwrite=force,
dry=dry,
)

+ 1
- 0
src/shut/commands/pkg/__init__.py

@ -33,3 +33,4 @@ def pkg():
from . import checks
from . import new
from . import status
from . import update

+ 9
- 3
src/shut/commands/pkg/update.py

@ -19,17 +19,23 @@
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
import click
from shut.commands import project
from shut.commands.commons.new import write_files
from shut.commands.pkg import pkg
from shut.model import PackageModel
from shut.generate import render_setuptools_files
from shut.update import get_files
@pkg.command()
def update():
@click.option('--force', is_flag=True)
@click.option('--dry', is_flag=True)
def update(force, dry):
"""
Update package files generated from the Package configuration.
"""
package = project.load(expect=PackageModel)
render_setuptools_files(package)
files = get_files(package)
write_files(files, package.get_directory(), force=force, dry=dry)

+ 34
- 3
src/shut/model/package.py

@ -21,6 +21,7 @@
import ast
import os
import re
from typing import Dict, Iterable, List, Optional
from databind.core import datamodel, field
@ -81,6 +82,29 @@ class PackageData:
def get_modulename(self) -> str:
return self.modulename or self.name.replace('-', '_')
def get_python_requirement(self) -> Optional[Requirement]:
return next(filter(lambda x: x.package == 'python', self.requirements), None)
def is_universal(self) -> bool:
"""
Checks if the package is a universal Python package (i.e. it is Python 2 and 3 compatible)
by testing the `$.requirements.python` version selector. If none is specified, the package
is also considered universal.
"""
if self.universal is not None:
return self.universal
python_requirement = self.get_python_requirement()
if not python_requirement:
return True
# TODO (@NiklasRosenstein): This method of detecting if the version selector
# selects a Python 2 and 3 version is very suboptimal.
has_2 = re.search(r'\b2\b|\b2\.\b', str(python_requirement))
has_3 = re.search(r'\b3\b|\b3\.\b', str(python_requirement))
return bool(has_2 and has_3)
@datamodel
class InstallConfiguration:
@ -91,6 +115,9 @@ class InstallConfiguration:
before_develop: List[str] = field(altname='before-develop', default_factory=list)
after_develop: List[str] = field(altname='after-develop', default_factory=list)
def any(self):
return any((self.before_install, self.after_install, self.before_develop, self.after_develop))
hooks: InstallHooks = field(default_factory=InstallHooks)
@ -190,11 +217,15 @@ class PythonPackageMetadata:
just a single Python module.
"""
dirname, basename = os.path.split(self.filename)
if basename not in ('__init__.py', '__version__.py'):
if self.is_single_module:
raise ValueError('this package is in module-only form')
return dirname
return os.path.dirname(self.filename) or '.'
@property
def is_single_module(self) -> bool:
basename = os.path.basename(self.filename)
return basename not in ('__init__.py', '__version__.py')
@property
def author(self) -> str:

+ 32
- 0
src/shut/update/__init__.py

@ -0,0 +1,32 @@
# -*- coding: utf8 -*-
# Copyright (c) 2020 Niklas Rosenstein
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
"""
The `shut.update` package implements rendering the files that can be produced from a monorepo
and package definition.
"""
from .core import (
register_renderer,
get_files,
)
from . import setuptools

+ 56
- 0
src/shut/update/core.py

@ -0,0 +1,56 @@
# -*- coding: utf8 -*-
# Copyright (c) 2020 Niklas Rosenstein
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
import abc
from typing import Generic, T, Type
from shut.utils.io.virtual import VirtualFiles
from shut.utils.type_registry import TypeRegistry
class Renderer(Generic[T], metaclass=abc.ABCMeta):
@abc.abstractmethod
def get_files(self, files: VirtualFiles, obj: T) -> None:
pass
registry = TypeRegistry[Type[Renderer]]()
def register_renderer(t: Type[T], renderer: Type[Renderer[T]]) -> None:
"""
Register the *renderer* implementation to run when creating files for *t*.
"""
registry.put(t, renderer)
def get_files(obj: T) -> VirtualFiles:
"""
Gets all the files from the renderers registered to the type of *obj*.
"""
files = VirtualFiles()
for renderer in registry.for_type(type(obj)):
renderer().get_files(files, obj)
return files

+ 333
- 0
src/shut/update/setuptools.py

@ -0,0 +1,333 @@
# -*- coding: utf8 -*-
# Copyright (c) 2020 Niklas Rosenstein
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
import os
import textwrap
from typing import Dict, List, Optional, TextIO, Tuple
import nr.fs
from shut.model import PackageModel
from shut.model.package import PackageData, PythonPackageMetadata, InstallConfiguration
from shut.model.requirements import Requirement
from shut.utils.io.virtual import VirtualFiles
from .core import Renderer, register_renderer
GENERATED_FILE_REMARK = '''
# This file was auto-generated by Shut. DO NOT EDIT
# For more information about Shut, check out https://pypi.org/project/shut/
'''.strip() + '\n'
def _normpath(x):
return os.path.normpath(x).replace(os.sep, '/')
def _get_readme_content_type(filename: str) -> str:
return {
'md': 'text/markdown',
'rst': 'text/x-rst',
}.get(nr.fs.getsuffix(filename), 'text/plain')
class SetuptoolsRenderer(Renderer[PackageModel]):
#: Begin an end section for the MANIFEST.in file.
_BEGIN_SECTION = '# This section is auto-generated by Shut. DO NOT EDIT {'
_END_SECTION = '# }'
#: These variables are used to format entrypoints in the setup.py file. It
#: allows the addition of the Python interpreter version to the entrypoint
#: names.
_ENTRTYPOINT_VARS = {
'python-major-version': 'sys.version[0]',
'python-major-minor-version': 'sys.version[:3]'
}
def get_files(self, files: VirtualFiles, package: PackageModel) -> None:
files.add_dynamic('setup.py', self._render_setup, package)
if package.data.typed:
directory = package.get_python_package_metadata().package_directory
files.add_static(os.path.join(directory, 'py.typed'), '')
def _render_setup(
self,
fp: TextIO,
package: PackageModel,
) -> None:
metadata = package.get_python_package_metadata()
install = package.install
data = package.data
# Write the header/imports.
fp.write(GENERATED_FILE_REMARK + '\n')
fp.write('from __future__ import print_function\n')
if install.hooks.before_install or install.hooks.after_install:
fp.write('from setuptools.command.install import install as _install_command\n')
if install.hooks.before_develop or install.hooks.after_develop:
fp.write('from setuptools.command.develop import develop as _develop_command\n')
fp.write(textwrap.dedent('''
import io
import os
import re
import setuptools
import sys
''').lstrip())
# Write hook overrides.
cmdclass = {}
if install.hooks.any():
fp.write('\ninstall_hooks = [\n')
for hook in package.install_hooks:
fp.write(' ' + json.dumps(hook.normalize().to_json(), sort_keys=True) + ',\n')
fp.write(']\n')
fp.write(textwrap.dedent('''
def _run_hooks(event):
import subprocess, shlex, os
def _shebang(fn):
with open(fn) as fp:
line = fp.readline()
if line.startswith('#'):
return shlex.split(line[1:].strip())
return []
for hook in install_hooks:
if not hook['event'] or hook['event'] == event:
command = [x.replace('$SHORE_INSTALL_HOOK_EVENT', event) for x in hook['command']]
if command[0].endswith('.py') or 'python' in _shebang(command[0]):
command.insert(0, sys.executable)
env = os.environ.copy()
env['SHORE_INSTALL_HOOK_EVENT'] = event
res = subprocess.call(command, env=env)
if res != 0:
raise RuntimeError('command {!r} returned exit code {}'.format(command, res))
'''))
if install.hooks.after_install or install.hooks.before_install:
fp.write(textwrap.dedent('''
class install_command(_install_command):
def run(self):
_run_hooks('install')
super(install_command, self).run()
_run_hooks('post-install')
'''))
cmdclass['install'] = 'install_command'
if install.hooks.before_develop or install.hooks.after_develop:
fp.write(textwrap.dedent('''
class develop_command(_develop_command):
def run(self):
_run_hooks('develop')
super(develop_command, self).run()
_run_hooks('post-develop')
'''))
cmdclass['develop'] = 'develop_command'
# Write the helper that extracts the version number from the entry file.
pkg_metadata_file = os.path.relpath(metadata.filename, package.get_directory())
fp.write(textwrap.dedent('''
with io.open({entrypoint_file!r}, encoding='utf8') as fp:
version = re.search(r"__version__\s*=\s*'(.*)'", fp.read()).group(1)
''').format(entrypoint_file=_normpath(pkg_metadata_file)))
readme_file, long_description_expr = self._render_readme_code(fp, package)
# Write the install requirements.
fp.write('\n')
self._render_requirements(fp, 'requirements', data.requirements)
if data.test_requirements:
self._render_requirements(fp, 'test_requirements', data.test_requirements)
tests_require = 'test_requirements'
else:
tests_require = '[]'
if data.extra_requirements:
fp.write('extra_requirements = {}\n')
for key, value in data.extra_requirements.items():
self._render_requirements(fp, 'extras_require[{!r}]'.format(key), value)
extras_require = 'extra_requirements'
else:
extras_require = '{}'
# TODO(NiklasRosenstein): Data files support
data_files = '[]'
exclude_packages = []
for pkg in data.exclude:
exclude_packages.append(pkg)
exclude_packages.append(pkg + '.*')
if metadata.is_single_module:
packages_args = ' py_modules = [{!r}],'.format(data.get_modulename())
else:
packages_args = ' packages = setuptools.find_packages({src_directory!r}, {exclude_packages!r}),'.format(
src_directory=data.source_directory,
exclude_packages=exclude_packages)
# Find the requirement on Python itself.
python_requirement = data.get_python_requirement()
if python_requirement:
python_requires_expr = repr(python_requirement.version.to_setuptools() if python_requirement else None)
else:
python_requires_expr = 'None'
# Write the setup function.
fp.write(textwrap.dedent('''
setuptools.setup(
name = {name!r},
version = version,
author = {author_name!r},
author_email = {author_email!r},
description = {description!r},
long_description = {long_description_expr},
long_description_content_type = {long_description_content_type!r},
url = {url!r},
license = {license!r},
{packages_args}
package_dir = {{'': {src_directory!r}}},
include_package_data = {include_package_data!r},
install_requires = requirements,
extras_require = {extras_require},
tests_require = {tests_require},
python_requires = {python_requires_expr},
data_files = {data_files},
entry_points = {entry_points},
cmdclass = {cmdclass},
keywords = {keywords!r},
classifiers = {classifiers!r},
''').rstrip().format(
name=data.name,
packages_args=packages_args,
author_name=data.author.name,
author_email=data.author.email,
url=data.url,
license=data.license,
description=data.description.replace('\n\n', '%%%%').replace('\n', ' ').replace('%%%%', '\n').strip(),
long_description_expr=long_description_expr,
long_description_content_type=_get_readme_content_type(readme_file) if readme_file else None,
extras_require=extras_require,
tests_require=tests_require,
python_requires_expr=python_requires_expr,
src_directory=data.source_directory,
include_package_data=True,#package.package_data != [],
data_files=data_files,
entry_points=self._render_entrypoints(data.entrypoints),
cmdclass = '{' + ', '.join('{!r}: {}'.format(k, v) for k, v in cmdclass.items()) + '}',
keywords = data.keywords,
classifiers = data.classifiers,
))
if data.is_universal():
fp.write(textwrap.dedent('''
options = {
'bdist_wheel': {
'universal': True,
},
},
)
'''))
else:
fp.write('\n)\n')
def _render_entrypoints(self, entrypoints: Dict[str, List[str]]) -> None:
if not entrypoints:
return '{}'
lines = ['{']
for key, value in entrypoints.items():
lines.append(' {!r}: ['.format(key))
for item in value:
item = repr(item)
args = []
for varname, expr in self._ENTRTYPOINT_VARS.items():
varname = '{{' + varname + '}}'
if varname in item:
item = item.replace(varname, '{' + str(len(args)) + '}')
args.append(expr)
if args:
item += '.format(' + ', '.join(args) + ')'
lines.append(' ' + item.strip() + ',')
lines.append(' ],')
lines[-1] = lines[-1][:-1]
lines.append(' }')
return '\n'.join(lines)
@staticmethod
def _format_reqs(reqs: List[Requirement], level: int = 0) -> List[str]:
indent = ' ' * (level + 1)
return '[\n' + ''.join(indent + '{!r},\n'.format(x.to_setuptools()) for x in reqs if x.package != 'python') + ']'
def _render_requirements(self, fp: TextIO, target: str, requirements: List[Requirement]):
fp.write('{} = {}\n'.format(target, self._format_reqs(requirements)))
def _render_readme_code(self, fp: TextIO, package: PackageModel) -> Tuple[Optional[str], Optional[str]]:
"""
Renders code for the setup.py file, creating a `long_description` variable. If
a readme file is present or explicitly specified in *package*, that readme file
will be read for the setup.
The readme file may be locatated outside of the packages' directory. In this case,
the setup.py file will temporarily copy it into the package root directory during
the setup.
Returns the Python expression to pass into the `long_description` field of the
#setuptools.setup() call.
"""
readme = package.get_readme_file()
if not readme:
return None, 'None'
# Make sure the readme is relative (we need it relative either way).
readme = os.path.relpath(readme, package.get_directory())
# If the readme file is _not_ inside the package directory, the setup.py will
# temporarily copy it. The filename at setup time is thus just the readme's
# base filename.
is_inside = nr.fs.issub(os.path.relpath(readme, package.get_directory()))
if is_inside:
readme_relative_path = readme
else:
readme_relative_path = os.path.basename(readme)
fp.write('\nreadme_file = {!r}\n'.format(readme_relative_path))
if not is_inside:
# Copy the relative README file if it exists.
fp.write(textwrap.dedent('''
source_readme_file = {!r}
if not os.path.isfile(readme_file) and os.path.isfile(source_readme_file):
import shutil; shutil.copyfile(source_readme_file, readme_file)
import atexit; atexit.register(lambda: os.remove(readme_file))
''').format(readme).lstrip())
# Read the contents of the file into the "long_description" variable.
fp.write(textwrap.dedent('''
if os.path.isfile(readme_file):
with io.open(readme_file, encoding='utf8') as fp:
long_description = fp.read()
else:
print("warning: file \\"{}\\" does not exist.".format(readme_file), file=sys.stderr)
long_description = None
''').lstrip())
return readme, 'long_description'
register_renderer(PackageModel, SetuptoolsRenderer)

+ 40
- 0
src/shut/utils/type_registry.py

@ -0,0 +1,40 @@
# -*- coding: utf8 -*-
# Copyright (c) 2020 Niklas Rosenstein
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
from typing import Iterable, Generic, T, Type
class TypeRegistry(Generic[T]):
"""
A helper class to register objects with types. The registered objects can be retrieved
back for a type, respecting registration and inheritance order.
"""
def __init__(self):
self._type_map = {}
def put(self, type_: Type, data: T) -> None:
self._type_map.setdefault(type_, []).append(data)
def for_type(self, type_: Type) -> Iterable[T]:
for base in type_.__bases__:
yield from self.for_type(base)
yield from self._type_map.get(type_, ())

Loading…
Cancel
Save