Browse Source

re-describe Package and Monorepo model using databind.core (instead of nr.databind.core)

shut-new-model
Niklas Rosenstein 9 months ago
parent
commit
0c55736fcc
No known key found for this signature in database GPG Key ID: 6D269B33D25F6C6
16 changed files with 278 additions and 141 deletions
  1. + 1
    - 1
      package.yaml
  2. + 2
    - 2
      src/shore/util/version.py
  3. + 1
    - 1
      src/shut/commands/pkg/__init__.py
  4. + 21
    - 15
      src/shut/commands/pkg/new.py
  5. + 35
    - 0
      src/shut/commands/pkg/update.py
  6. + 23
    - 16
      src/shut/model/__init__.py
  7. + 13
    - 18
      src/shut/model/author.py
  8. + 4
    - 3
      src/shut/model/changelog.py
  9. + 3
    - 2
      src/shut/model/linter.py
  10. + 12
    - 11
      src/shut/model/monorepo.py
  11. + 44
    - 39
      src/shut/model/package.py
  12. + 8
    - 6
      src/shut/model/release.py
  13. + 15
    - 17
      src/shut/model/requirements.py
  14. + 11
    - 10
      src/shut/model/version.py
  15. + 22
    - 0
      src/shut/render/__init__.py
  16. + 63
    - 0
      src/shut/render/core.py

+ 1
- 1
package.yaml

@ -1,6 +1,6 @@
package:
name: shut
version: "1.0.1"
version: "0.1.0"
license: MIT
url: https://git.niklasrosenstein.com/NiklasRosenstein/shore
description: Automates the heavy lifting of release and distribution management for pure Python packages.

+ 2
- 2
src/shore/util/version.py

@ -67,12 +67,12 @@ class Version(_Version):
return other < self and other != self
def __eq__(self, other):
if super().__eq__(other):
if super().__eq__(other) is True:
return (self.commit_distance, self.sha) == (other.commit_distance, other.sha)
return False
def __ne__(self, other):
return not self == other
return not (self == other)
@property
def pep440_compliant(self):

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

@ -30,6 +30,6 @@ def pkg():
"""
from . import bootstrap
from . import checks
from . import new
from . import status

src/shut/commands/pkg/bootstrap.py → src/shut/commands/pkg/new.py

@ -19,12 +19,16 @@
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
from shore.util.license import get_license_metadata, wrap_license_text
from shut.model import dump
from shut.model.author import Author
from shut.model.package import PackageModel, PackageData
from shut.model.requirements import Requirement, VersionSelector
from shut.model.version import Version
from shut.utils.io.virtual import VirtualFiles
from . import pkg
from shore.model import Author, Package, RootRequirements, VersionSelector
from shore.util.version import Version
from shore.util.license import get_license_metadata, wrap_license_text
from termcolor import colored
from typing import Optional
import click
@ -78,7 +82,7 @@ def load_author_from_git() -> Optional[str]:
@pkg.command(no_args_is_help=True)
@click.argument('target_directory', required=False)
@click.option('--project-name', metavar='name', required=True, help='The name of the project as it would appear on PyPI.')
@click.option('--project-name', '--name', metavar='name', required=True, help='The name of the project as it would appear on PyPI.')
@click.option('--module-name', metavar='fqn', help='The name of the main Python module (this may be a dotted module name). Defaults to the package name (hyphens replaced with underscores).')
@click.option('--author', metavar='"name <mail>"', type=Author.parse, help='The name of the author to write into the package configuration file. Defaults to the name and email from the Git config.')
@click.option('--version', metavar='x.y.z', help='The version number to start counting from. Defaults to "0.0.0" (stands for "unreleased").')
@ -88,7 +92,7 @@ def load_author_from_git() -> Optional[str]:
@click.option('--suffix', type=click.Choice(['yaml', 'yml']), help='The suffix for YAML files. Defaults to "yml".', default='yml')
@click.option('--dry', is_flag=True, help='Do not write files to disk.')
@click.option('-f', '--force', is_flag=True, help='Overwrite files if they already exist.')
def bootstrap(
def new(
target_directory,
project_name,
module_name,
@ -141,15 +145,17 @@ def bootstrap(
if not version:
version = version or Version('0.0.0')
package_manifest = Package(
name=project_name,
modulename=None if module_name == project_name.replace('-', '_') else module_name,
version=version,
author=author,
license=license,
description=description or 'Package description here.',
requirements=RootRequirements(
python=VersionSelector('^2.7|^3.5' if universal else '^3.5'),
package_manifest = PackageModel(
data=PackageData(
name=project_name,
modulename=None if module_name == project_name.replace('-', '_') else module_name,
version=version,
author=author,
license=license,
description=description or 'Package description here.',
requirements=[
Requirement('python', VersionSelector('^2.7|^3.5' if universal else '^3.5')),
],
),
)
@ -169,7 +175,7 @@ def bootstrap(
files.add_static('.gitignore', GITIGNORE_TEMPLATE)
files.add_dynamic('README.md', _render_template, README_TEMPLATE)
files.add_dynamic('package.' + suffix, lambda fp: package_manifest.dump(fp))
files.add_dynamic('package.' + suffix, lambda fp: dump(package_manifest, fp))
files.add_dynamic(
'src/{}/__init__.py'.format(module_name.replace('.', '/')),

+ 35
- 0
src/shut/commands/pkg/update.py

@ -0,0 +1,35 @@
# -*- 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 shut.commands import project
from shut.commands.pkg import pkg
from shut.model import PackageModel
from shut.generate import render_setuptools_files
@pkg.command()
def update():
"""
Update package files generated from the Package configuration.
"""
package = project.load(expect=PackageModel)
render_setuptools_files(package)

+ 23
- 16
src/shut/model/__init__.py

@ -19,22 +19,22 @@
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
from typing import List, Tuple, Type, TypeVar, Union
import os
import sys
from typing import Any, List, T, TextIO, Tuple, Type, TypeVar, Union
from nr.databind.core import ObjectMapper, NodeCollector, SerializationError
from nr.databind.json import JsonModule
from databind.core import datamodel, field, Registry
from databind.json import from_json, to_json, registry as json_registry
from nr.stream import Stream
import yaml
import os
import sys
registry = Registry(json_registry)
registry.set_option(datamodel, 'skip_defaults', True)
ExcInfo = Tuple
from .monorepo import MonorepoModel
from .package import PackageModel
T = TypeVar('T')
ExcInfo = Tuple
def get_existing_file(directory: str, choices: List[str]) -> bool:
for fn in choices:
@ -53,9 +53,8 @@ class Project:
monorepo_filenames = ['monorepo.yml', 'monorepo.yaml']
package_filenames = ['package.yml', 'package.yaml']
def __init__(self, mapper: ObjectMapper = None):
def __init__(self):
self._cache: Dict[str, Union[MonorepoModel, PackageModel]] = {}
self.mapper = mapper or ObjectMapper(JsonModule())
self.subject: Union[MonorepoModel, PackageModel] = None
self.monorepo: MonorepoModel = None
self.packages: List[PackageModel] = []
@ -108,13 +107,12 @@ class Project:
return obj
with open(filename) as fp:
data = yaml.safe_load(fp)
node_collector = NodeCollector()
obj = self._cache[filename] = self.mapper.deserialize(
data, type_, filename=filename, decorations=[node_collector])
#node_collector = NodeCollector()
obj = self._cache[filename] = from_json(type_, data, registry=registry)
obj.filename = filename
obj.unknown_keys = list(Stream.concat(
(x.locator.append(k) for k in x.unknowns)
for x in node_collector.nodes))
#obj.unknown_keys = list(Stream.concat(
# (x.locator.append(k) for k in x.unknowns)
# for x in node_collector.nodes))
return obj
def _load_monorepo(self, filename: str) -> MonorepoModel:
@ -137,3 +135,12 @@ class Project:
if package not in self.packages:
self.packages.append(package)
return package
def dump(obj: Any, file_: Union[str, TextIO]) -> None:
if isinstance(file_, str):
with open(file_, 'w') as fp:
dump(fp, obj)
else:
data = to_json(obj, registry=registry)
yaml.safe_dump(data, file_, sort_keys=False)

+ 13
- 18
src/shut/model/author.py

@ -19,22 +19,21 @@
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
from nr.databind.core import Field, Struct
from databind.core import datamodel
from nr.databind.json import JsonSerializer
import re
@JsonSerializer(serialize='_serialize', deserialize='_deserialize')
class Author(Struct):
@datamodel
class Author:
"""
Represents information about an author. Can be deserialized from a string of
the form `Name <user@domain.name>`
Represents information about an author. Can be deserialized from a string of the form
`Name <user@domain.name>`
"""
AUTHOR_EMAIL_REGEX = re.compile(r'([^<]+)<([^>]+)>')
name = Field(str)
email = Field(str)
name: str
email: str
@classmethod
def parse(cls, string: str) -> 'Author':
@ -49,14 +48,10 @@ class Author(Struct):
return '{} <{}>'.format(self.name, self.email)
@classmethod
def _serialize(cls, mapper, node) -> str:
return str(node.value)
def databind_json_load(cls, value, context):
if isinstance(value, str):
return cls.parse(value)
return NotImplemented
@classmethod
def _deserialize(cls, mapper, node) -> 'Author':
if isinstance(node.value, str):
try:
return cls.parse(node.value)
except ValueError:
pass
raise NotImplementedError
def databind_json_dump(self, context):
return str(self)

+ 4
- 3
src/shut/model/changelog.py

@ -19,8 +19,9 @@
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
from nr.databind.core import Field, FieldName, Struct
from databind.core import datamodel
class ChangelogConfiguration(Struct):
directory = Field(str, default='.changelog')
@datamodel
class ChangelogConfiguration:
directory: str = '.changelog'

+ 3
- 2
src/shut/model/linter.py

@ -19,9 +19,10 @@
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
from nr.databind.core import Field, FieldName, Struct
from databind.core import datamodel
class LinterConfiguration(Struct):
@datamodel
class LinterConfiguration:
# TODO
pass

+ 12
- 11
src/shut/model/monorepo.py

@ -19,19 +19,20 @@
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
from typing import List, Optional
from databind.core import datamodel, field
from .author import Author
from .version import Version
from .release import MonorepoReleaseConfiguration
from nr.databind.core import Field, FieldName, Struct
from typing import List
class MonorepoModel(Struct):
filename = Field(str, hidden=True, default=None)
unknown_keys = Field(List[str], hidden=True, default=list)
name = Field(str)
version = Field(Version, default=None)
author = Field(Author, default=None)
license = Field(str, default=None)
url = Field(str, default=None)
release = Field(MonorepoReleaseConfiguration, default=Field.DEFAULT_CONSTRUCT)
@datamodel
class MonorepoModel:
filename: Optional[str] = field(derived=True, default=None)
unknown_keys: List[str] = field(derived=True, default_factory=list)
name: str
version: Optional[Version] = None
author: Optional[Author] = None
license: str = None
url: str = None
release: MonorepoReleaseConfiguration = field(default_factory=MonorepoReleaseConfiguration)

+ 44
- 39
src/shut/model/package.py

@ -19,6 +19,10 @@
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
import ast
import os
from typing import Dict, Iterable, List, Optional
from databind.core import datamodel, field
from shore.util.ast import load_module_members
from .author import Author
@ -27,10 +31,6 @@ from .linter import LinterConfiguration
from .release import ReleaseConfiguration
from .requirements import Requirement
from .version import Version
from nr.databind.core import Field, FieldName, Struct
from typing import Dict, Iterable, List, Optional
import ast
import os
def _get_file_in_directory(directory: str, prefix: str, preferred: List[str]) -> Optional[str]:
@ -53,49 +53,54 @@ def _get_file_in_directory(directory: str, prefix: str, preferred: List[str]) ->
return os.path.join(directory, name)
class PackageData(Struct):
name = Field(str)
modulename = Field(str, default=None)
version = Field(Version, default=None)
author = Field(Author)
description = Field(str, default=None)
license = Field(str, default=None)
url = Field(str, default=None)
readme = Field(str, default=None)
wheel = Field(bool, default=True)
universal = Field(bool, default=None)
typed = Field(bool, default=False)
requirements = Field(List[Requirement], default=list)
test_requirements = Field(List[Requirement], FieldName('test-requirements'), default=list)
extra_requirements = Field(Dict[str, List[Requirement]], FieldName('extra-requirements'), default=dict)
source_directory = Field(str, FieldName('source-directory'), default='src')
exclude = Field(List[str], default=lambda: ['test', 'tests', 'docs'])
entrypoints = Field(Dict[str, List[str]], default=dict)
classifiers = Field(List[str], default=list)
keywords = Field(List[str], default=list)
@datamodel
class PackageData:
name: str
modulename: Optional[str] = None
version: Optional[Version] = None
author: Author
description: Optional[str] = None
license: Optional[str] = None
url: Optional[str] = None
readme: Optional[str] = None
wheel: Optional[bool] = True
universal: Optional[bool] = None
typed: Optional[bool] = False
requirements: List[Requirement] = field(default_factory=list)
test_requirements: List[Requirement] = field(altname='test-requirements', default_factory=list)
extra_requirements: Dict[str, List[Requirement]] = field(altname='extra-requirements', default_factory=dict)
source_directory: str = field(altname='source-directory', default='src')
exclude: List[str] = field(default_factory=lambda: ['test', 'tests', 'docs'])
entrypoints: Dict[str, List[str]] = field(default_factory=dict)
classifiers: List[str] = field(default_factory=list)
keywords: List[str] = field(default_factory=list)
# TODO: Data files
def get_modulename(self) -> str:
return self.modulename or self.name.replace('-', '_')
class InstallConfiguration(Struct):
hooks = Field(dict(
before_install=Field(List[str], FieldName('before-install'), default=list),
after_install=Field(List[str], FieldName('after-install'), default=list),
before_develop=Field(List[str], FieldName('before-develop'), default=list),
after_develop=Field(List[str], FieldName('after-develop'), default=list),
), default=Field.DEFAULT_CONSTRUCT)
@datamodel
class InstallConfiguration:
@datamodel
class InstallHooks:
before_install: List[str] = field(altname='before-install', default_factory=list)
after_install: List[str] = field(altname='after-install', default_factory=list)
before_develop: List[str] = field(altname='before-develop', default_factory=list)
after_develop: List[str] = field(altname='after-develop', default_factory=list)
hooks: InstallHooks = field(default_factory=InstallHooks)
class PackageModel(Struct):
filename = Field(str, hidden=True, default=None)
unknown_keys = Field(List[str], hidden=True, default=list)
data = Field(PackageData, FieldName('package'))
changelog = Field(ChangelogConfiguration, default=Field.DEFAULT_CONSTRUCT)
install = Field(InstallConfiguration, default=Field.DEFAULT_CONSTRUCT)
linter = Field(LinterConfiguration, default=Field.DEFAULT_CONSTRUCT)
release = Field(ReleaseConfiguration, default=Field.DEFAULT_CONSTRUCT)
@datamodel
class PackageModel:
filename: Optional[str] = field(derived=True, default=None)
unknown_keys: List[str] = field(derived=True, default_factory=list)
data: PackageData = field(altname='package')
changelog: ChangelogConfiguration = field(default_factory=ChangelogConfiguration)
install: InstallConfiguration = field(default_factory=InstallConfiguration)
linter: LinterConfiguration = field(default_factory=LinterConfiguration)
release: ReleaseConfiguration = field(default_factory=ReleaseConfiguration)
def get_python_package_metadata(self) -> 'PythonPackageMetadata':
"""

+ 8
- 6
src/shut/model/release.py

@ -19,13 +19,15 @@
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
from nr.databind.core import Field, FieldName, Struct
from databind.core import datamodel, field
class ReleaseConfiguration(Struct):
private = Field(bool, default=False)
tag_format = Field(str, FieldName('tag-format'), default='{version}')
@datamodel
class ReleaseConfiguration:
private: bool = False
tag_format: str = field(altname='tag-format', default='{version}')
class MonorepoReleaseConfiguration(Struct):
single_version = Field(bool, FieldName('single-version'), default=False)
@datamodel
class MonorepoReleaseConfiguration:
single_version: bool = field(altname='single-version', default=False)

+ 15
- 17
src/shut/model/requirements.py

@ -19,10 +19,10 @@
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
from .version import Version
from nr.databind.json import JsonSerializer
from typing import Union
import re
from typing import Union
from databind.core import datamodel
from .version import Version
class VersionSelector(object):
@ -102,19 +102,14 @@ class VersionSelector(object):
VersionSelector.ANY = VersionSelector('*')
@JsonSerializer(deserialize='_deserialize')
class Requirement(object):
@datamodel
class Requirement:
"""
A Requirement is simply combination of a package name and a version selector.
"""
def __init__(self, package, version): # type: (str, VersionSelector)
if not isinstance(package, str):
raise TypeError('expected str for package_name')
if not isinstance(version, VersionSelector):
raise TypeError('expected VersionSelector for version')
self.package = package
self.version = version
package: str
version: VersionSelector
def __str__(self):
if self.version == VersionSelector.ANY:
@ -122,7 +117,7 @@ class Requirement(object):
return '{} {}'.format(self.package, self.version)
def __repr__(self):
return repr(str(self))#'Requirement({!r})'.format(str(self))
return repr(str(self))
@classmethod
def parse(cls, requirement_string):
@ -138,7 +133,10 @@ class Requirement(object):
return '{} {}'.format(self.package, self.version.to_setuptools())
@classmethod
def _deserialize(cls, mapper, node):
if not isinstance(node.value, str):
raise node.type_error()
return Requirement.parse(node.value)
def databind_json_load(cls, value, context):
if isinstance(value, str):
return Requirement.parse(value)
return NotImplemented
def databind_json_dump(self, context):
return str(self)

+ 11
- 10
src/shut/model/version.py

@ -19,17 +19,18 @@
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
from nr.databind.json import JsonSerializer
from shore.util.version import parse_version, Version as _Version
from databind.core import Context, Converter
from shore.util.version import parse_version, Version
from . import registry
@JsonSerializer(serialize='_serialize', deserialize='_deserialize')
class Version(_Version):
class VersionConverter(Converter):
@classmethod
def _serialize(cls, mapper, node) -> str:
return str(node.value)
def from_python(self, value, context):
return str(value)
@classmethod
def _deserialize(cls, mapper, node) -> 'Version':
return Version(parse_version(node.value))
def to_python(self, value, context):
return parse_version(value)
registry.register_converter(Version, VersionConverter())

+ 22
- 0
src/shut/render/__init__.py

@ -0,0 +1,22 @@
# -*- 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 .setuptools import render_setuptools_files

+ 63
- 0
src/shut/render/core.py

@ -0,0 +1,63 @@
# -*- 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.
class FileToRender(object):
"""
Represents a file that can be rendered to disk on-demand.
# Arguments
filename: The file on disk that should be rendered.
render_func: The function that will be called to render the file's contents. The
function must accept a file-like object as it's first argument. If *convolutional*
is set to `True`, it must also accept an additional file-like object that represents
the current contents of the file, or None if the file did not previously exist on
disk.
args: Additional positional arguments for *render_func*.
convolutional: Whether *render_func* accepts a second file-like object argument.
encoding: The encoding of the file object to pass.
kwargs: Additional keyword arguments for *render_func*.
"""
def __init__(
self,
filename: str,
render_callback: Callable,
*args: Any,
convolutional: bool = False,
encoding: str = None,
**kwargs: Any,
) -> None:
super(FileToRender, self).__init__()
self.filename =
self.name = nr.fs.norm(nr.fs.join(directory or '.', name))
self.encoding = kwargs.pop('encoding', self.encoding)
self._callable = callable
self._args = args
self._kwargs = kwargs
def with_chmod(self, chmod):
self.chmod = chmod
return self
@override
def render(self, current, dst):
return self._callable(current, dst, *self._args, *self._kwargs)

Loading…
Cancel
Save