Browse Source

add builders and publishers concepts

shut-new-model
Niklas Rosenstein 9 months ago
parent
commit
7c4285838c
No known key found for this signature in database GPG Key ID: 6D269B33D25F6C6
28 changed files with 722 additions and 29 deletions
  1. + 3
    - 3
      MANIFEST.in
  2. + 25
    - 0
      src/shut/builders/__init__.py
  3. + 69
    - 0
      src/shut/builders/core.py
  4. + 170
    - 0
      src/shut/builders/setuptools.py
  5. + 4
    - 9
      src/shut/checkers/__init__.py
  6. + 0
    - 0
      src/shut/checkers/core.py
  7. + 0
    - 0
      src/shut/checkers/generic.py
  8. + 0
    - 0
      src/shut/checkers/monorepo.py
  9. + 0
    - 0
      src/shut/checkers/package.py
  10. + 1
    - 1
      src/shut/commands/commons/bump.py
  11. + 5
    - 3
      src/shut/commands/commons/checks.py
  12. + 1
    - 1
      src/shut/commands/mono/checks.py
  13. + 1
    - 1
      src/shut/commands/mono/update.py
  14. + 2
    - 0
      src/shut/commands/pkg/__init__.py
  15. + 69
    - 0
      src/shut/commands/pkg/build.py
  16. + 1
    - 1
      src/shut/commands/pkg/checks.py
  17. + 87
    - 0
      src/shut/commands/pkg/publish.py
  18. + 1
    - 1
      src/shut/commands/pkg/update.py
  19. + 2
    - 0
      src/shut/model/package.py
  20. + 61
    - 0
      src/shut/model/publish.py
  21. + 57
    - 0
      src/shut/model/target.py
  22. + 25
    - 0
      src/shut/publish/__init__.py
  23. + 59
    - 0
      src/shut/publish/core.py
  24. + 66
    - 0
      src/shut/publish/warehouse.py
  25. + 4
    - 8
      src/shut/renderers/__init__.py
  26. + 8
    - 0
      src/shut/renderers/core.py
  27. + 0
    - 0
      src/shut/renderers/generic.py
  28. + 1
    - 1
      src/shut/renderers/setuptools.py

+ 3
- 3
MANIFEST.in

@ -1,5 +1,5 @@
# This section is auto-generated by Shut. DO NOT EDIT {
package.yaml
README.md
LICENSE.txt
include package.yaml
include README.md
include LICENSE.txt
# }

+ 25
- 0
src/shut/builders/__init__.py

@ -0,0 +1,25 @@
# -*- 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 .core import *
from . import setuptools
__all__ = core.__all__

+ 69
- 0
src/shut/builders/core.py

@ -0,0 +1,69 @@
# -*- 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, Iterable, List, T, Type
from nr.stream import concat
from shut.model import AbstractProjectModel
from shut.model.target import Target, TargetId
from shut.utils.type_registry import TypeRegistry
__all__ = [
'Builder',
'BuilderProvider',
'register_builder_provider',
'get_builders',
]
class Builder(Target, metaclass=abc.ABCMeta):
@abc.abstractmethod
def get_outputs(self) -> Iterable[str]:
"""
Returns a list of the output files produced by this builder.
"""
@abc.abstractmethod
def build(self, build_directory: str, verbose: bool) -> bool:
"""
Run the build. Output from subprocesses should be captured unless *verbose* is enabled.
"""
class BuilderProvider(Generic[T], metaclass=abc.ABCMeta):
@abc.abstractmethod
def get_builders(self, obj: T) -> List[Builder]:
pass
registry = TypeRegistry[AbstractProjectModel]()
def register_builder_provider(type_: Type[T], provider_class: Type[BuilderProvider[T]]) -> None:
registry.put(type_, provider_class)
def get_builders(obj: T) -> Iterable[Builder]:
return concat(provider().get_builders(obj) for provider in registry.for_type(type(obj)))

+ 170
- 0
src/shut/builders/setuptools.py

@ -0,0 +1,170 @@
# -*- 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 subprocess
import sys
from typing import Iterable, List, Optional
from termcolor import colored
from shut.model import PackageModel
from shut.model.target import TargetId
from . import Builder, BuilderProvider, register_builder_provider
class SetuptoolsBuilder(Builder):
"""
Internal. Implements building a Python package.
"""
_FORMATS_MAP = {
'zip': '.zip',
'gztar': '.tar.gz',
'bztar': '.tar.bz2',
'ztar': '.tar.Z',
'tar': '.tar'
}
def __init__(
self,
id_: TargetId,
description: str,
output_files: List[str],
package_directory: str,
build_type: str,
args: List[str],
) -> None:
self._id = id_
self.description = description
self.output_files = output_files
self.package_directory = package_directory
self.build_type = build_type
self.args = args
def __repr__(self):
return f'SetuptoolsBuilder(id={self.id!r}, build_type={self.build_type!r}, args={self.args!r})'
@classmethod
def wheel(
cls,
id_: TargetId,
description: str,
package: PackageModel,
) -> 'SetuptoolsBuilder':
py = 'py2.py3' if package.data.is_universal() else ('py' + sys.version[0])
filename = f'{package.data.name.replace("-", "_")}-{package.data.version}-{py}-none-any.whl'
return cls(id_, description, [filename], package.get_directory(), 'bdist_wheel', [])
@classmethod
def sdist(
cls,
id_: TargetId,
description: str,
formats: List[str],
package: PackageModel,
) -> 'SetuptoolsBuilder':
assert formats
return cls(
id_,
description,
[f'{package.data.name}-{package.data.version}{cls._FORMATS_MAP[f]}' for f in formats],
package.get_directory(),
'sdist',
['--format', ','.join(formats)],
)
# Builder Overrides
def get_description(self) -> Optional[str]:
return self.description
def get_outputs(self) -> Iterable[str]:
return self.output_files
def build(self, build_directory: str, verbose: bool) -> bool:
# TODO: Can we change the distribution output directory with an option?
python = os.getenv('PYTHON', sys.executable)
dist_directory = os.path.join(self.package_directory, 'dist')
dist_exists = os.path.exists(dist_directory)
command = [python, 'setup.py', self.build_type] + self.args
proc = subprocess.Popen(
command,
cwd=self.package_directory,
stdout=None if verbose else subprocess.PIPE,
stderr=None if verbose else subprocess.PIPE)
stdout, stderr = proc.communicate()
if stderr:
for line in stderr.decode().splitlines():
if not line:
continue
print(f' {colored(line, "red")}')
res = proc.wait()
if res != 0:
return False
# Make sure the files end up in the correct directory.
for filename in self.output_files:
src = next(filter(os.path.isfile, [
os.path.join(dist_directory, filename),
os.path.join(dist_directory, filename.lower())]), None)
if not src:
raise RuntimeError('{} not produced by setup.py {}'.format(filename, self.build_type))
dst = os.path.join(build_directory, filename)
if src != dst:
if os.path.isfile(dst):
os.remove(dst)
os.rename(src, dst)
# Cleanup after yourself.
if not dist_exists:
shutil.rmtree(dist_directory)
return True
# Target Overrides
@property
def id(self) -> TargetId:
return self._id
class SetuptoolsBuilderProvider(BuilderProvider[PackageModel]):
# BuilderProvider Overrides
def get_builders(self, package: PackageModel) -> Iterable[Builder]:
yield SetuptoolsBuilder.sdist(
TargetId('setuptools', 'sdist'),
'Build a source distribute',
['gztar'],
package,
)
if package.data.wheel:
yield SetuptoolsBuilder.wheel(
TargetId('setuptools', 'wheel'),
'Build a Python wheel.',
package,
)
register_builder_provider(PackageModel, SetuptoolsBuilderProvider)

src/shut/checks/__init__.py → src/shut/checkers/__init__.py

@ -20,16 +20,11 @@
# IN THE SOFTWARE.
"""
The `shut.checks` package implements all the sanity checks that are run over a monoreop and
The `shut.checkers` 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,
Check,
register_checker,
get_checks,
)
from .core import *
from . import generic, monorepo, package
__all__ = core.__all__

src/shut/checks/core.py → src/shut/checkers/core.py


src/shut/checks/generic.py → src/shut/checkers/generic.py


src/shut/checks/monorepo.py → src/shut/checkers/monorepo.py


src/shut/checks/package.py → src/shut/checkers/package.py


+ 1
- 1
src/shut/commands/commons/bump.py

@ -36,7 +36,7 @@ from shut.changelog.manager import ChangelogManager
from shut.commands import project
from shut.model import AbstractProjectModel, Project
from shut.model.version import bump_version, parse_version, Version
from shut.update import get_version_refs, VersionRef
from shut.renderers import get_version_refs, VersionRef
from shut.utils.text import substitute_ranges
logger = logging.getLogger(__name__)

+ 5
- 3
src/shut/commands/commons/checks.py

@ -19,11 +19,13 @@
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
from shut.checks import Check, CheckStatus
import sys
import time
from typing import List
import termcolor
import time
import sys
from shut.checkers import Check, CheckStatus
def print_checks(

+ 1
- 1
src/shut/commands/mono/checks.py

@ -19,7 +19,7 @@
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
from shut.checks import CheckStatus, get_checks
from shut.checkers import CheckStatus, get_checks
from shut.commands import project
from shut.commands.commons.checks import print_checks_all, get_checks_status
from shut.commands.mono import mono

+ 1
- 1
src/shut/commands/mono/update.py

@ -25,7 +25,7 @@ from shut.commands import project
from shut.commands.commons.new import write_files
from shut.commands.pkg.update import update_package
from shut.model import MonorepoModel
from shut.update import get_files
from shut.renderers import get_files
from . import mono

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

@ -30,9 +30,11 @@ def pkg():
"""
from . import build
from . import bump
from . import checks
from . import new
from . import publish
from . import requirements
from . import status
from . import update

+ 69
- 0
src/shut/commands/pkg/build.py

@ -0,0 +1,69 @@
# -*- 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 sys
import click
from nr.stream import groupby
from termcolor import colored
from shut.builders import Builder, get_builders
from shut.model import PackageModel
from shut.model.target import TargetId
from . import pkg
from .. import project
@pkg.command()
@click.argument('target', type=lambda s: TargetId.parse(s, True), required=False)
@click.option('-l', '--list', 'list_', is_flag=True, help='list available builders')
@click.option('-b', '--build-dir', default='build', help='build output directory')
@click.option('-v', '--verbose', is_flag=True, help='show more output')
def build(target, list_, build_dir, verbose):
"""
Produce a build of the package.
"""
if target and list_:
sys.exit('error: conflicting options')
package = project.load_or_exit(expect=PackageModel)
builders = list(get_builders(package))
if list_:
print()
for scope, builders in groupby(builders, lambda b: b.id.scope):
print(f'{colored(scope, "green")}:')
for builder in builders:
print(f' {builder.id.name} – {builder.get_description()}')
print()
return
if not target:
sys.exit('error: no target specified')
builders = [b for b in builders if target.match(b.id)]
if not builders:
sys.exit(f'error: no target matches "{target}"')
for builder in builders:
print(colored(f'building {colored(builder.id, "green")}'))
builder.build(build_dir, verbose)

+ 1
- 1
src/shut/commands/pkg/checks.py

@ -31,7 +31,7 @@ import termcolor
from nr.stream import Stream
from termcolor import colored
from shut.checks import CheckStatus, get_checks
from shut.checkers import CheckStatus, get_checks
from shut.commands import project
from shut.commands.commons.checks import print_checks_all, get_checks_status
from shut.model import PackageModel, Project

+ 87
- 0
src/shut/commands/pkg/publish.py

@ -0,0 +1,87 @@
# -*- 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 List
import click
from shut.model import PackageModel
from shut.publish import Publisher
from shut.publish.warehouse import WarehousePublisher
from . import pkg
from .. import project
def get_publisher(package: PackageModel, target: str) -> Publisher:
if target == 'pypi' and package.data.publish.pypi:
publisher = WarehousePublisher.pypi_from_credentials(
package.data.publish.pypi_credentials)
elif target in package.data.publish.warehouses:
publisher = WarehousePublisher.from_config(package.data.publish.warehouses[target])
else:
raise ValueError(f'unknown publish target {target!r}')
return publisher
def get_publisher_names(package: PackageModel) -> List[str]:
targets = []
if package.data.publish.pypi.enabled:
targets.append('pypi')
targets.extend(package.data.publish.warehouses.keys())
return targets
@pkg.command()
@click.argument('target')
@click.option('--ls', is_flag=True)
def publish(target, ls):
"""
Publish the package to PyPI or another target.
"""
if ls and target:
sys.exit('error: conflicting options')
if ls:
names = get_publisher_names()
if not names:
print('no publishes configured')
else:
print('available publishers:')
for name in names:
print(f' {name}')
return
package = project.load_or_exit(expect=PackageModel)
try:
publisher = get_publisher(package, target)
except ValueError as exc:
sys.exit(f'error: {exc}')
if isinstance(publisher, WarehousePublisher):
build_targets = get_build_targets('setuptools')
else:
raise RuntimeError
run_build_targets(build_targets)
publisher.publish(build_targets)

+ 1
- 1
src/shut/commands/pkg/update.py

@ -24,7 +24,7 @@ import click
from shut.commands import project
from shut.commands.commons.new import write_files
from shut.model import PackageModel
from shut.update import get_files
from shut.renderers import get_files
from . import pkg

+ 2
- 0
src/shut/model/package.py

@ -31,6 +31,7 @@ from .abstract import AbstractProjectModel
from .author import Author
from .changelog import ChangelogConfiguration
from .linter import LinterConfiguration
from .publish import PublishConfiguration
from .release import ReleaseConfiguration
from .requirements import Requirement
from .version import Version
@ -134,6 +135,7 @@ class PackageModel(AbstractProjectModel):
data: PackageData = field(altname='package')
install: InstallConfiguration = field(default_factory=InstallConfiguration)
linter: LinterConfiguration = field(default_factory=LinterConfiguration)
publish: PublishConfiguration = field(default_factory=PublishConfiguration)
def get_python_package_metadata(self) -> 'PythonPackageMetadata':
"""

+ 61
- 0
src/shut/model/publish.py

@ -0,0 +1,61 @@
# -*- 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 Dict, Optional
from databind.core import datamodel, field
@datamodel
class WarehouseCredentials:
username: Optional[str] = None
password: Optional[str] = None
test_username: Optional[str] = None
test_password: Optional[str] = None
@datamodel
class WarehouseConfiguration(WarehouseCredentials):
repository: Optional[str] = None
repository_url: Optional[str] = None
test_repository: Optional[str] = None
test_repository_url: Optional[str] = None
@datamodel
class PypiConfiguration:
#: Whether publishing to PyPI is enabled.
enabled: bool = True
#: The credentials configuration for PyPI. Variables in the from `$(VARNAME)`
#: will be substituted from environment variables.
credentials: WarehouseCredentials = field(
altname='credentials',
default_factory=WarehouseCredentials)
@datamodel
class PublishConfiguration:
# Configuration for PyPI.
pypi: PypiConfiguration = field(default_factory=PypiConfiguration)
#: Additional warehouse targets to publish to.
warehouses: Dict[str, WarehouseConfiguration] = field(default_factory=dict)

+ 57
- 0
src/shut/model/target.py

@ -0,0 +1,57 @@
# -*- 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 fnmatch import fnmatch
from databind.core import datamodel
@datamodel(frozen=True)
class TargetId:
"""
Represents the ID of a target that can be selected on the command line. We use the
"target" concept for builds and publishers. A target ID may contain wildcard characters
in which case it can be used to match multiple targets with #fnmatch().
"""
scope: str
name: str
def __str__(self):
return f'{self.scope}:{self.name}'
@classmethod
def parse(cls, s: str, allow_scope_only: bool = False) -> 'TargetId':
parts = s.split(':')
if allow_scope_only and len(parts) == 1:
parts = (parts[0], '*')
return cls(*parts)
def match(self, other_id: 'TargetId') -> bool:
return fnmatch(other_id.scope, self.scope) and fnmatch(other_id.name, self.name)
class Target(metaclass=abc.ABCMeta):
@abc.abstractproperty
def id(self) -> TargetId:
pass

+ 25
- 0
src/shut/publish/__init__.py

@ -0,0 +1,25 @@
# -*- 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 .core import *
from . import warehouse
__all__ = core.__all__

+ 59
- 0
src/shut/publish/core.py

@ -0,0 +1,59 @@
# -*- 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, Iterable, List, T, Type
from shut.model import AbstractProjectModel
from shut.model.target import Target, TargetId
from shut.utils.type_registry import TypeRegistry
__all__ = [
'Publisher',
'PublisherProvider',
'register_publisher_provider',
'get_publishers',
]
class Publisher(Target, metaclass=abc.ABCMeta):
@abc.abstractmethod
def publish(self, verbose: bool) -> bool:
pass
class PublisherProvider(Generic[T], metaclass=abc.ABCMeta):
@abc.abstractmethod
def get_publishers(self) -> List[Publisher]:
pass
registry = TypeRegistry[PublisherProvider[AbstractProjectModel]]()
def register_publisher_provider(type_: Type[T], provider_class: Type[PublisherProvider[T]]) -> None:
registry.put(type_, provider_class)
def get_publishers(obj: T) -> Iterable[Publisher]:
return concat(provider().get_publishers(obj) for provider in registry.for_type(type(obj)))

+ 66
- 0
src/shut/publish/warehouse.py

@ -0,0 +1,66 @@
# -*- 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
from shut.model import PackageModel, Project
from shut.model.publish import WarehouseCredentials, WarehouseConfiguration
from shut.model.target import TargetId
from .core import Publisher, PublisherProvider, register_publisher_provider
class WarehousePublisher(Publisher):
def __init__(self, id_: TargetId, config: WarehouseConfiguration) -> None:
self._id = id_
self.config = config
@classmethod
def from_pypi_credentials(cls, id_: TargetId, creds: WarehouseCredentials) -> 'WarehousePublisher':
return cls(id_, WarehouseCredentials().with_creds(creds))
# Publisher Overrides
def publish(self):
raise NotImplementedError('todo')
# Target Overrides
@property
def id(self) -> TargetId:
return self._id
class WarehouseProvider(PublisherProvider[PackageModel]):
# PublisherProvider Overrides
def get_publishers(self) -> Iterable[Publisher]:
if package.data.publish.pypi.enabled:
yield WarehousePublisher.from_pypi_credentials(
TargetId('warehouse', 'pypi'),
package.data.publish.pypi.credentials)
for name, config in package.data.publish.warehouse.items():
yield WarehouseProvider(TargetId('warehouse', name), config)
register_publisher_provider(PackageModel, WarehouseProvider)

src/shut/update/__init__.py → src/shut/renderers/__init__.py

@ -20,15 +20,11 @@
# IN THE SOFTWARE.
"""
The `shut.update` package implements rendering the files that can be produced from a monorepo
This package implements rendering the files that can be produced from a monorepo
and package definition.
"""
from .core import (
get_files,
get_version_refs,
register_renderer,
VersionRef
)
from .core import *
from . import generic, setuptools
__all__ = core.__all__

src/shut/update/core.py → src/shut/renderers/core.py

@ -27,6 +27,14 @@ from databind.core import datamodel
from shut.utils.io.virtual import VirtualFiles
from shut.utils.type_registry import TypeRegistry
__all__ = [
'VersionRef',
'Renderer',
'register_renderer',
'get_files',
'get_version_refs',
]
@datamodel
class VersionRef:

src/shut/update/generic.py → src/shut/renderers/generic.py


src/shut/update/setuptools.py → src/shut/renderers/setuptools.py

@ -374,7 +374,7 @@ class SetuptoolsRenderer(Renderer[PackageModel]):
markers = (self._BEGIN_SECTION, self._END_SECTION)
with _rewrite_section(fp, current.read() if current else '', *markers):
for entry in manifest:
fp.write('{}\n'.format(entry))
fp.write('include {}\n'.format(entry))
# Renderer[PackageModel] Overrides

Loading…
Cancel
Save