xproject-python 0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,220 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ pip-wheel-metadata/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ # Usually these files are written by a python script from a template
32
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
33
+ *.manifest
34
+ *.spec
35
+
36
+ # Installer logs
37
+ pip-log.txt
38
+ pip-delete-this-directory.txt
39
+
40
+ # Unit test / coverage reports
41
+ htmlcov/
42
+ .tox/
43
+ .nox/
44
+ .stestr
45
+ .coverage
46
+ .coverage.*
47
+ .cache
48
+ nosetests.xml
49
+ coverage.xml
50
+ *.cover
51
+ *.py.cover
52
+ .hypothesis/
53
+ .pytest_cache/
54
+ cover/
55
+
56
+ # Translations
57
+ *.mo
58
+ *.pot
59
+
60
+ # Django stuff:
61
+ *.log
62
+ local_settings.py
63
+ db.sqlite3
64
+ db.sqlite3-journal
65
+
66
+ # Flask stuff:
67
+ instance/
68
+ .webassets-cache
69
+
70
+ # Scrapy stuff:
71
+ .scrapy
72
+
73
+ # Sphinx documentation
74
+ docs/_build/
75
+
76
+ # PyBuilder
77
+ .pybuilder/
78
+ target/
79
+
80
+ # Jupyter Notebook
81
+ .ipynb_checkpoints
82
+
83
+ # IPython
84
+ profile_default/
85
+ ipython_config.py
86
+
87
+ # pyenv
88
+ # For a library or package, you might want to ignore these files since the code is
89
+ # intended to run in multiple environments; otherwise, check them in:
90
+ # .python-version
91
+
92
+ # pipenv
93
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
94
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
95
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
96
+ # install all needed dependencies.
97
+ # Pipfile.lock
98
+
99
+ # UV
100
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
101
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
102
+ # commonly ignored for libraries.
103
+ # uv.lock
104
+
105
+ # poetry
106
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
107
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
108
+ # commonly ignored for libraries.
109
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
110
+ # poetry.lock
111
+ # poetry.toml
112
+
113
+ # pdm
114
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
115
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
116
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
117
+ # pdm.lock
118
+ # pdm.toml
119
+ .pdm-python
120
+ .pdm-build/
121
+
122
+ # pixi
123
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
124
+ # pixi.lock
125
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
126
+ # in the .venv directory. It is recommended not to include this directory in version control.
127
+ .pixi
128
+
129
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
130
+ __pypackages__/
131
+
132
+ # Celery stuff
133
+ celerybeat-schedule
134
+ celerybeat.pid
135
+
136
+ # Redis
137
+ *.rdb
138
+ *.aof
139
+ *.pid
140
+
141
+ # RabbitMQ
142
+ mnesia/
143
+ rabbitmq/
144
+ rabbitmq-data/
145
+
146
+ # ActiveMQ
147
+ activemq-data/
148
+
149
+ # SageMath parsed files
150
+ *.sage.py
151
+
152
+ # Environments
153
+ .env
154
+ .envrc
155
+ .venv
156
+ env/
157
+ venv/
158
+ ENV/
159
+ env.bak/
160
+ venv.bak/
161
+
162
+ # Spyder project settings
163
+ .spyderproject
164
+ .spyproject
165
+
166
+ # Rope project settings
167
+ .ropeproject
168
+
169
+ # mkdocs documentation
170
+ /site
171
+
172
+ # mypy
173
+ .mypy_cache/
174
+ .dmypy.json
175
+ dmypy.json
176
+
177
+ # Pyre type checker
178
+ .pyre/
179
+
180
+ # pytype static type analyzer
181
+ .pytype/
182
+
183
+ # Cython debug symbols
184
+ cython_debug/
185
+
186
+ # PyCharm
187
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
188
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
189
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
190
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
191
+ # .idea/
192
+
193
+ # Abstra
194
+ # Abstra is an AI-powered process automation framework.
195
+ # Ignore directories containing user credentials, local state, and settings.
196
+ # Learn more at https://abstra.io/docs
197
+ .abstra/
198
+
199
+ # Visual Studio Code
200
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
201
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
202
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
203
+ # you could uncomment the following to ignore the entire vscode folder
204
+ # .vscode/
205
+ # Temporary file for partial code execution
206
+ tempCodeRunnerFile.py
207
+
208
+ # Ruff stuff:
209
+ .ruff_cache/
210
+
211
+ # PyPI configuration file
212
+ .pypirc
213
+
214
+ # Marimo
215
+ marimo/_static/
216
+ marimo/_lsp/
217
+ __marimo__/
218
+
219
+ # Streamlit
220
+ .streamlit/secrets.toml
@@ -0,0 +1 @@
1
+ ../LICENSE
@@ -0,0 +1,19 @@
1
+ Metadata-Version: 2.4
2
+ Name: xproject-python
3
+ Version: 0.1
4
+ Summary: Initialize project files
5
+ Project-URL: Homepage, https://github.com/bondbox/xproject/
6
+ Author-email: Mingzhe Zou <zoumingzhe@outlook.com>
7
+ License-File: LICENSE
8
+ Keywords: project
9
+ Classifier: Programming Language :: Python
10
+ Classifier: Programming Language :: Python :: 3
11
+ Requires-Python: >=3.8
12
+ Requires-Dist: xkits-command>=0.6
13
+ Requires-Dist: xkits-config-toml>=0.7
14
+ Requires-Dist: xkits-file>=0.10
15
+ Description-Content-Type: text/markdown
16
+
17
+ # xproject
18
+
19
+ > Initialize project files
@@ -0,0 +1,46 @@
1
+ [build-system]
2
+ build-backend = "hatchling.build"
3
+ requires = [ "hatch-requirements-txt", "hatchling", "xpip-build>=1.4",]
4
+
5
+ [project]
6
+ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3",]
7
+ description = "Initialize project files"
8
+ dynamic = [ "dependencies", "version",]
9
+ keywords = [ "project",]
10
+ license-files = [ "LICENSE",]
11
+ name = "xproject-python"
12
+ requires-python = ">=3.8"
13
+ [[project.authors]]
14
+ name = "Mingzhe Zou"
15
+ email = "zoumingzhe@outlook.com"
16
+
17
+ [project.readme]
18
+ content-type = "text/markdown"
19
+ file = "readme.md"
20
+
21
+ [project.scripts]
22
+ xproject-python-generate = "xproject_python.projector:main"
23
+ xproject-python-config = "xproject_python.configure:main"
24
+
25
+ [project.urls]
26
+ Homepage = "https://github.com/bondbox/xproject/"
27
+
28
+ [tool.hatch.version]
29
+ path = "xproject_python/attribute.py"
30
+
31
+ [tool.hatch.build.targets.sdist]
32
+ exclude = [ "xproject_python/unittest",]
33
+ packages = [ "xproject_python",]
34
+
35
+ [tool.hatch.build.targets.wheel]
36
+ exclude = [ "xproject_python/unittest",]
37
+ packages = [ "xproject_python",]
38
+
39
+ [tool.hatch.metadata.hooks.requirements_txt]
40
+ files = [ "requirements.txt",]
41
+
42
+ [tool.hatch.build.targets.sdist.force-include]
43
+ templates = "xproject_python/templates"
44
+
45
+ [tool.hatch.build.targets.wheel.force-include]
46
+ templates = "xproject_python/templates"
@@ -0,0 +1 @@
1
+ ../readme.md
File without changes
@@ -0,0 +1,8 @@
1
+ # coding:utf-8
2
+
3
+ __version__ = "0.1"
4
+
5
+ # project info
6
+ __project_name__ = "xproject"
7
+ __project_home__ = "https://github.com/bondbox/xproject/"
8
+ __project_desc__ = "Initialize project files"
@@ -0,0 +1,162 @@
1
+ # coding:utf-8
2
+
3
+ from dataclasses import dataclass
4
+ from dataclasses import field
5
+ from errno import ENOENT
6
+ from typing import Dict
7
+ from typing import List
8
+ from typing import Optional
9
+ from typing import Sequence
10
+
11
+ from xkits_command.actuator import Command
12
+ from xkits_command.actuator import CommandArgument
13
+ from xkits_command.actuator import CommandExecutor
14
+ from xkits_command.parser import ArgParser
15
+ from xkits_config import Settings
16
+ from xkits_config_toml import ConfigTOML
17
+
18
+ from xproject_python.attribute import __project_home__ as xproject_home
19
+ from xproject_python.attribute import __project_name__ as xproject_name
20
+ from xproject_python.attribute import __version__ as version
21
+
22
+ DEFAULT_CONFIG_FILE: str = f".{xproject_name}_python"
23
+
24
+
25
+ @dataclass
26
+ class AuthorConfig(Settings):
27
+ name: str
28
+ email: str
29
+
30
+
31
+ @dataclass
32
+ class ModuleConfig(Settings):
33
+ base: Optional[str] = None
34
+ package: Optional[List[str]] = None
35
+ omitted: List[str] = field(default_factory=list)
36
+ exclude: List[str] = field(default_factory=list)
37
+ include: Dict[str, str] = field(default_factory=dict)
38
+ scripts: Dict[str, str] = field(default_factory=dict)
39
+ templates: Dict[str, bool] = field(default_factory=dict)
40
+
41
+
42
+ @dataclass
43
+ class PackageConfig(Settings): # pylint: disable=too-many-instance-attributes
44
+ base: Optional[str] = None
45
+ version: Optional[str] = None
46
+ requires_python: Optional[str] = None
47
+ authors: List[str] = field(default_factory=list)
48
+ keywords: List[str] = field(default_factory=list)
49
+ requirements: List[str] = field(default_factory=list)
50
+ modules: Dict[str, ModuleConfig] = field(default_factory=dict)
51
+ max_complexity: int = 10
52
+ max_line_length: int = 127
53
+
54
+
55
+ @dataclass
56
+ class ProjectConfig(ConfigTOML):
57
+ name: str = xproject_name
58
+ home: str = xproject_home
59
+ description: str = f"Automatically created by [{xproject_name}]({xproject_home})." # noqa:E501
60
+ authors: Dict[str, AuthorConfig] = field(default_factory=dict)
61
+ keywords: List[str] = field(default_factory=list)
62
+ packages: Dict[str, PackageConfig] = field(default_factory=dict)
63
+ version: str = "0.1.alpha.1"
64
+
65
+
66
+ @CommandArgument("update", help="Update Python project configuration")
67
+ def add_cmd_config_update(_arg: ArgParser):
68
+ pass
69
+
70
+
71
+ @CommandExecutor(add_cmd_config_update)
72
+ def run_cmd_config_update(cmds: Command) -> int:
73
+ try:
74
+ ProjectConfig.loadf(cmds.args.file).dumpf(cmds.args.file)
75
+ cmds.stderr_green(f"Configuration file {cmds.args.file} updated")
76
+ return 0
77
+ except FileNotFoundError:
78
+ cmds.stderr_red(f"Configuration file {cmds.args.file} not found")
79
+ return ENOENT
80
+
81
+
82
+ @CommandArgument("config", help="Manage Python project configuration")
83
+ def add_cmd_config(_arg: ArgParser):
84
+ _arg.add_argument("--file", dest="file", type=str, nargs=None,
85
+ metavar="FILE", default=DEFAULT_CONFIG_FILE,
86
+ help="Specify configuration file")
87
+
88
+
89
+ @CommandExecutor(add_cmd_config, add_cmd_config_update)
90
+ def run_cmd_config(cmds: Command) -> int: # pylint: disable=unused-argument
91
+ return 0
92
+
93
+
94
+ def main(argv: Optional[Sequence[str]] = None) -> int:
95
+ cmds = Command()
96
+ cmds.version = version
97
+ return cmds.run(root=add_cmd_config, argv=argv, epilog=f"For more, please visit {xproject_home}.") # noqa:E501
98
+
99
+
100
+ if __name__ == "__main__":
101
+ project_name: str = "xproject"
102
+ project_home: str = "https://github.com/bondbox/xproject/"
103
+ project_desc: str = "Initialize project files"
104
+
105
+ ProjectConfig(
106
+ name=project_name,
107
+ home=project_home,
108
+ description=project_desc,
109
+ authors={
110
+ "zoumingzhe": AuthorConfig(
111
+ name="Mingzhe Zou",
112
+ email="zoumingzhe@outlook.com",
113
+ ),
114
+ },
115
+ keywords=[
116
+ "project",
117
+ ],
118
+ packages={
119
+ f"{project_name}-python": PackageConfig(
120
+ base=f"{project_name}-python",
121
+ version=None,
122
+ requires_python=">=3.8",
123
+ authors=[
124
+ "zoumingzhe",
125
+ ],
126
+ keywords=[
127
+ ],
128
+ requirements=[
129
+ "xkits-command",
130
+ "xkits-config-toml>=0.5",
131
+ "xkits-file>=0.9",
132
+ "prompt-toolkit",
133
+ ],
134
+ modules={
135
+ f"{project_name}-python": ModuleConfig(
136
+ base=f"{project_name}_python",
137
+ package=None,
138
+ omitted=[
139
+ "attribute.py",
140
+ "unittest/*",
141
+ ],
142
+ exclude=[
143
+ "unittest",
144
+ ],
145
+ include={
146
+ "templates": "templates",
147
+ },
148
+ scripts={
149
+ f"{project_name}-python": "blueprint:main",
150
+ },
151
+ templates={
152
+ "__init__.py": False,
153
+ "attribute.py": True,
154
+ },
155
+ ),
156
+ },
157
+ max_complexity=15,
158
+ max_line_length=127,
159
+ ),
160
+ },
161
+ version="0.1.alpha.1",
162
+ ).dumpf(DEFAULT_CONFIG_FILE)
@@ -0,0 +1,367 @@
1
+ # coding:utf-8
2
+
3
+ from errno import ENOENT
4
+ from json import dumps
5
+ from pathlib import Path
6
+ from sys import version_info
7
+ from typing import Dict
8
+ from typing import Iterator
9
+ from typing import List
10
+ from typing import Optional
11
+ from typing import Sequence
12
+ from typing import Tuple
13
+ from typing import Union
14
+
15
+ from packaging.specifiers import SpecifierSet
16
+ from xkits_command.actuator import Command
17
+ from xkits_command.actuator import CommandArgument
18
+ from xkits_command.actuator import CommandExecutor
19
+ from xkits_command.parser import ArgParser
20
+ from xkits_file.template import TemplateManagerPath
21
+ from xkits_file.template import Variable
22
+
23
+ from xproject_python.attribute import __project_home__ as project_home
24
+ from xproject_python.attribute import __version__ as version
25
+ from xproject_python.configure import AuthorConfig
26
+ from xproject_python.configure import DEFAULT_CONFIG_FILE
27
+ from xproject_python.configure import ModuleConfig
28
+ from xproject_python.configure import PackageConfig
29
+ from xproject_python.configure import ProjectConfig
30
+ from xproject_python.utilities import CoverageRC
31
+ from xproject_python.utilities import Flake8
32
+ from xproject_python.utilities import PylintRC
33
+ from xproject_python.utilities import Pyproject
34
+ from xproject_python.utilities import Requirements
35
+
36
+ TEMPLATES: Path = Path(__file__).parent / "templates"
37
+
38
+
39
+ class Module:
40
+ TEMPLATES_MODULE: Path = TEMPLATES / "module"
41
+ ATTRIBUTE: str = "attribute.py"
42
+ DOT: str = "."
43
+
44
+ def __init__(self, name: str, config: PackageConfig, variable: Optional[Variable] = None): # noqa:E501
45
+ option: ModuleConfig = config.modules[name]
46
+
47
+ variables: Variable = variable.duplicate()if isinstance(variable, Variable) else Variable() # noqa:E501
48
+ variables.set_default("module_name", module_name := self.normalize(name)) # noqa:E501
49
+
50
+ self.__name: str = module_name
51
+ self.__option: ModuleConfig = option
52
+ self.__variable: Variable = variables
53
+
54
+ @property
55
+ def name(self) -> str:
56
+ return self.__name
57
+
58
+ @property
59
+ def base(self) -> str:
60
+ return self.option.base or self.DOT
61
+
62
+ @property
63
+ def option(self) -> ModuleConfig:
64
+ return self.__option
65
+
66
+ @property
67
+ def variable(self) -> Variable:
68
+ return self.__variable
69
+
70
+ @property
71
+ def include(self) -> Iterator[Tuple[str, str]]:
72
+ for name, path in self.option.include.items():
73
+ yield name, self.path_join(path)
74
+
75
+ @property
76
+ def exclude(self) -> Iterator[str]:
77
+ for path in self.option.exclude:
78
+ yield self.path_join(path)
79
+
80
+ @property
81
+ def package(self) -> Iterator[str]:
82
+ if self.option.package is not None:
83
+ for path in self.option.package:
84
+ yield self.path_join(path)
85
+ else:
86
+ yield self.base
87
+
88
+ @property
89
+ def omitted(self) -> Iterator[str]:
90
+ for path in self.option.omitted:
91
+ yield self.path_join(path)
92
+
93
+ @property
94
+ def scripts(self) -> Iterator[Tuple[str, str]]:
95
+ for name, entry in self.option.scripts.items():
96
+ point: str = (parts := entry.rsplit(":", maxsplit=1)).pop()
97
+ yield name, ":".join([self.dot_join(*parts), point])
98
+
99
+ def prepend(self, *parts: str) -> Tuple[str, ...]:
100
+ return (base, *parts) if (base := self.base) != self.DOT else parts # noqa:E501
101
+
102
+ def dot_join(self, *parts: str) -> str:
103
+ return self.DOT.join(self.prepend(*parts))
104
+
105
+ def path_join(self, *parts: str) -> str:
106
+ return Path(*self.prepend(*parts)).as_posix()
107
+
108
+ @classmethod
109
+ def normalize(cls, name: str) -> str:
110
+ return Requirements.normalize(requirement=name).name.replace("-", "_")
111
+
112
+ def dump(self, base: Union[str, Path], writable: bool = False) -> None:
113
+ root: Path = base if isinstance(base, Path) else Path(base)
114
+
115
+ files: List[str] = [
116
+ name for name, edit in self.option.templates.items()
117
+ if not (root / name).exists() or edit
118
+ ]
119
+
120
+ templates: TemplateManagerPath = TemplateManagerPath(self.variable)
121
+ templates.load(base=self.TEMPLATES_MODULE, include=files)
122
+ templates.dump(base=root, writable=writable)
123
+
124
+
125
+ class Package: # pylint: disable=too-many-instance-attributes
126
+ TEMPLATES_PACKAGE: Path = TEMPLATES / "package"
127
+ FILES: List[str] = ["Makefile"]
128
+
129
+ def __init__(self, name: str, config: ProjectConfig, variable: Optional[Variable] = None): # pylint: disable=too-many-locals # noqa:E501
130
+ option: PackageConfig = config.packages[name]
131
+
132
+ variables: Variable = variable.duplicate() if isinstance(variable, Variable) else Variable() # noqa:E501
133
+ variables.set_default("package_name", package_name := Requirements.normalize(requirement=name).name) # noqa:E501
134
+ variables.set_default("package_version", package_version := option.version or config.version) # noqa:E501
135
+
136
+ authors: List[AuthorConfig] = [config.authors[index] for index in option.authors] # noqa:E501
137
+ variables.set_default("authors", dumps(authors, indent=4, default=lambda i: i.__dict__)) # noqa:E501
138
+
139
+ coverage: CoverageRC = CoverageRC.load(self.TEMPLATES_PACKAGE / CoverageRC.FILENAME) # noqa:E501
140
+ flake8: Flake8 = Flake8.load(self.TEMPLATES_PACKAGE / Flake8.FILENAME)
141
+ pylint: PylintRC = PylintRC.load(self.TEMPLATES_PACKAGE / PylintRC.FILENAME) # noqa:E501
142
+
143
+ python_version: str = option.requires_python or f"{version_info.major}.{version_info.minor}" # noqa:E501
144
+ pyproject: Pyproject = Pyproject.load(self.TEMPLATES_PACKAGE / Pyproject.FILENAME) # noqa:E501
145
+ pyproject.project["name"] = package_name
146
+ pyproject.project["description"] = config.description
147
+ pyproject.project["requires-python"] = python_version
148
+ pyproject.project_authors.extend(author.__dict__ for author in authors)
149
+ pyproject.project_keywords.extend(config.keywords)
150
+ pyproject.project_keywords.extend(option.keywords)
151
+ pyproject.project_urls["Homepage"] = config.home
152
+
153
+ requirements: Requirements = Requirements()
154
+ for requirement in option.requirements:
155
+ requirements.add(requirement)
156
+
157
+ self.__name: str = package_name
158
+ self.__version: str = package_version
159
+ self.__option: PackageConfig = option
160
+ self.__variable: Variable = variables
161
+ self.__coverage: CoverageRC = coverage
162
+ self.__flake8: Flake8 = flake8
163
+ self.__pylint: PylintRC = pylint
164
+ self.__pyproject: Pyproject = pyproject
165
+ self.__requirements: Requirements = requirements
166
+
167
+ def __iter__(self) -> Iterator[Module]:
168
+ for name in self.option.modules:
169
+ yield Module(name=name, config=self.option, variable=self.variable)
170
+
171
+ @property
172
+ def name(self) -> str:
173
+ return self.__name
174
+
175
+ @property
176
+ def version(self) -> str:
177
+ return self.__version
178
+
179
+ @property
180
+ def base(self) -> str:
181
+ return self.option.base or "."
182
+
183
+ @property
184
+ def option(self) -> PackageConfig:
185
+ return self.__option
186
+
187
+ @property
188
+ def variable(self) -> Variable:
189
+ return self.__variable
190
+
191
+ @property
192
+ def coverage(self) -> CoverageRC:
193
+ return self.__coverage
194
+
195
+ @property
196
+ def flake8(self) -> Flake8:
197
+ return self.__flake8
198
+
199
+ @property
200
+ def pylint(self) -> PylintRC:
201
+ return self.__pylint
202
+
203
+ @property
204
+ def pyproject(self) -> Pyproject:
205
+ return self.__pyproject
206
+
207
+ @property
208
+ def requirements(self) -> Requirements:
209
+ return self.__requirements
210
+
211
+ def dump(self, base: Union[str, Path], writable: bool = False) -> None: # pylint: disable=too-many-locals # noqa:E501
212
+ root: Path = base if isinstance(base, Path) else Path(base)
213
+ modules: List[Module] = list(iter(self))
214
+
215
+ attribute_modules: List[Module] = []
216
+ coverage_omit: str = ""
217
+ coverage_source: str = ""
218
+ flake8_exclude: str = ""
219
+ flake8_modules: List[str] = []
220
+ pylint_files: List[str] = []
221
+
222
+ for module in sorted(modules, key=lambda m: m.base):
223
+ for include_name, include_path in module.include:
224
+ self.pyproject.tool_hatch_build_targets_sdist_force_include[include_name] = include_path # noqa:E501
225
+ self.pyproject.tool_hatch_build_targets_wheel_force_include[include_name] = include_path # noqa:E501
226
+
227
+ for exclude in module.exclude:
228
+ self.pyproject.tool_hatch_build_targets_sdist_exclude.append(exclude) # noqa:E501
229
+ self.pyproject.tool_hatch_build_targets_wheel_exclude.append(exclude) # noqa:E501
230
+ flake8_exclude += f"\n{exclude}"
231
+
232
+ for package in module.package:
233
+ self.pyproject.tool_hatch_build_targets_sdist_packages.append(package) # noqa:E501
234
+ self.pyproject.tool_hatch_build_targets_wheel_packages.append(package) # noqa:E501
235
+ coverage_source += f"\n{package.removesuffix('.py')}"
236
+ flake8_modules.append(package)
237
+
238
+ pylint_files.append(f"{module.base}/*.py")
239
+
240
+ for omitted in module.omitted:
241
+ coverage_omit += f"\n{omitted}"
242
+
243
+ for script_name, script_entry in module.scripts:
244
+ self.pyproject.project_scripts[script_name] = script_entry
245
+
246
+ if Module.ATTRIBUTE in module.option.templates:
247
+ attribute_modules.append(module)
248
+
249
+ if (attribute_module_number := len(attribute_modules)) > 1:
250
+ raise ValueError(f"Package {self.name} has more than one attribute module: {', '.join(m.name for m in attribute_modules)}") # noqa:E501
251
+ if attribute_module_number < 1:
252
+ raise ValueError(f"Package {self.name} has no attribute module")
253
+
254
+ self.coverage.parser["run"]["omit"] = coverage_omit
255
+ self.coverage.parser["run"]["source"] = coverage_source
256
+ self.flake8.parser["flake8"]["exclude"] = flake8_exclude
257
+ self.flake8.parser["flake8"]["max-complexity"] = str(self.option.max_complexity) # noqa:E501
258
+ self.flake8.parser["flake8"]["max-line-length"] = str(self.option.max_line_length) # noqa:E501
259
+
260
+ variables: Variable = self.variable
261
+ variables.set_default("attribute_module", attribute_modules[0].dot_join(Path(Module.ATTRIBUTE).stem)) # noqa:E501
262
+ variables.set_default("flake8_modules", " ".join(flake8_modules))
263
+ variables.set_default("pylint_files", " ".join(pylint_files))
264
+
265
+ templates: TemplateManagerPath = TemplateManagerPath(variables)
266
+ templates.load(base=self.TEMPLATES_PACKAGE, include=self.FILES)
267
+ templates.dump(base=root, writable=writable)
268
+
269
+ self.requirements.dumpf(filepath=root / Requirements.FILENAME, writable=writable) # noqa:E501
270
+ self.pyproject.tool_hatch_metadata_hooks_requirements_txt_files.append(Requirements.FILENAME) # noqa:E501
271
+ self.pyproject.tool_hatch_version["path"] = attribute_modules[0].path_join(Module.ATTRIBUTE) # noqa:E501
272
+ self.pyproject.dump(filepath=root / Pyproject.FILENAME, writable=writable) # noqa:E501
273
+
274
+ self.coverage.dump(filepath=root / CoverageRC.FILENAME, writable=writable) # noqa:E501
275
+ self.flake8.dump(filepath=root / Flake8.FILENAME, writable=writable) # noqa:E501
276
+ self.pylint.dump(filepath=root / PylintRC.FILENAME, writable=writable) # noqa:E501
277
+
278
+ for module in modules:
279
+ module.dump(base=root / module.base, writable=writable)
280
+
281
+
282
+ class Project:
283
+ TEMPLATES_PROJECT: Path = TEMPLATES / "project"
284
+
285
+ def __init__(self, config: ProjectConfig):
286
+ project_name: str = Requirements.normalize(requirement=config.name).name # noqa:E501
287
+
288
+ variables: Variable = Variable(
289
+ project_name=project_name,
290
+ project_home=config.home,
291
+ project_description=config.description,
292
+ )
293
+
294
+ self.__name: str = project_name
295
+ self.__option: ProjectConfig = config
296
+ self.__variable: Variable = variables
297
+
298
+ def __iter__(self) -> Iterator[Package]:
299
+ for name in self.option.packages:
300
+ yield Package(name=name, config=self.option, variable=self.variable) # noqa:E501
301
+
302
+ @property
303
+ def name(self) -> str:
304
+ return self.__name
305
+
306
+ @property
307
+ def option(self) -> ProjectConfig:
308
+ return self.__option
309
+
310
+ @property
311
+ def variable(self) -> Variable:
312
+ return self.__variable
313
+
314
+ def dump(self, base: Union[str, Path], writable: bool = False) -> None:
315
+ root: Path = base if isinstance(base, Path) else Path(base)
316
+
317
+ templates: TemplateManagerPath = TemplateManagerPath(self.variable)
318
+ templates.load(base=self.TEMPLATES_PROJECT, include=None)
319
+ templates.dump(base=root, writable=writable)
320
+
321
+ packages: Dict[str, Package] = {package.name: package for package in iter(self)} # noqa:E501
322
+
323
+ for package in packages.values():
324
+ dest: Path = root / package.base
325
+
326
+ for requirement in package.requirements:
327
+ if dependence := packages.get(requirement.name):
328
+ requirement.specifier = SpecifierSet(f">={dependence.version}") # noqa:E501
329
+
330
+ package.dump(base=dest, writable=writable)
331
+
332
+ if dest != root:
333
+ if not (readme_link := dest / "readme.md").exists():
334
+ readme_link.symlink_to("../readme.md")
335
+
336
+
337
+ @CommandArgument("generate", help="Create or update Python project files")
338
+ def add_cmd_generate(_arg: ArgParser):
339
+ _arg.add_opt_on("--change", help="allow changes to existing files")
340
+ _arg.add_opt("--config", dest="config", type=str, nargs=None,
341
+ metavar="FILE", default=DEFAULT_CONFIG_FILE,
342
+ help="Specify configuration file")
343
+ _arg.add_pos("root", type=str, nargs="?", metavar="PATH", default=".",
344
+ help="Project root directory")
345
+
346
+
347
+ @CommandExecutor(add_cmd_generate)
348
+ def run_cmd_generate(cmds: Command) -> int:
349
+ try:
350
+ config: ProjectConfig = ProjectConfig.loadf(cmds.args.config)
351
+ except FileNotFoundError:
352
+ cmds.stderr_red(f"Configuration file {cmds.args.config} not found")
353
+ return ENOENT
354
+
355
+ root: Path = Path(cmds.args.root).resolve()
356
+ cmds.stderr_yellow(f"Generate to root directory: {root}")
357
+
358
+ project: Project = Project(config=config)
359
+ project.dump(base=root, writable=cmds.args.change)
360
+ cmds.stderr_green(f"Project {project.name} generated")
361
+ return 0
362
+
363
+
364
+ def main(argv: Optional[Sequence[str]] = None) -> int:
365
+ cmds = Command()
366
+ cmds.version = version
367
+ return cmds.run(root=add_cmd_generate, argv=argv, epilog=f"For more, please visit {project_home}.") # noqa:E501
@@ -0,0 +1,8 @@
1
+ # coding:utf-8
2
+
3
+ __version__ = "{package_version}"
4
+
5
+ # project info
6
+ __project_name__ = "{project_name}"
7
+ __project_home__ = "{project_home}"
8
+ __project_desc__ = "{project_description}"
@@ -0,0 +1,12 @@
1
+ [run]
2
+ omit =
3
+ source =
4
+
5
+ [report]
6
+ exclude_lines =
7
+ pragma: no cover
8
+ raise NotImplementedError
9
+ if __name__ == .__main__.:
10
+ def __repr__
11
+ pass
12
+ fail_under = 100
@@ -0,0 +1,9 @@
1
+ [flake8]
2
+ count = True
3
+ exclude =
4
+ filename = *.py
5
+ max-complexity =
6
+ max-line-length =
7
+ show-source = True
8
+ statistics = True
9
+ exit-zero = False
@@ -0,0 +1,7 @@
1
+ [MASTER]
2
+ disable =
3
+ C0103, # invalid-name
4
+ C0114, # missing-module-docstring
5
+ C0115, # missing-class-docstring
6
+ C0116, # missing-function-docstring
7
+ C0301, # line-too-long
@@ -0,0 +1,59 @@
1
+ MAKEFLAGS += --always-make
2
+
3
+ VERSION ?= $(shell python3 -c "from {attribute_module} import __version__; print(__version__)")
4
+
5
+ all: build test
6
+
7
+
8
+ release: all
9
+ if [ -n "${{VERSION}}" ]; then \
10
+ git tag -a v${{VERSION}} -m "release v${{VERSION}}"; \
11
+ git push origin --tags; \
12
+ fi
13
+
14
+ version:
15
+ @echo ${{VERSION}}
16
+
17
+
18
+ upload:
19
+ python3 -m pip install --upgrade xpip-upload
20
+ xpip-upload --config-file .pypirc dist/*
21
+
22
+
23
+ build-prepare:
24
+ python3 -m pip install --upgrade xpip-build
25
+ build-clean:
26
+ find . -type d -name "__pycache__" -exec rm -rf {{}} +
27
+ rm -rf build dist *.egg-info
28
+ build: build-prepare build-clean
29
+ python3 -m build --sdist --wheel
30
+
31
+
32
+ install-requirements:
33
+ python3 -m pip install --upgrade -r requirements.txt
34
+ install: install-requirements
35
+ python3 -m pip install --force-reinstall --no-deps dist/*.whl
36
+ uninstall:
37
+ python3 -m pip uninstall -y {package_name}
38
+ reinstall: uninstall install
39
+
40
+
41
+ test-prepare: install-requirements
42
+ python3 -m pip install --upgrade mock flake8 pylint pytest pytest-cov
43
+ flake8:
44
+ flake8 {flake8_modules}
45
+ pylint:
46
+ pylint $(shell git ls-files {pylint_files})
47
+ pytest:
48
+ pytest --cov --cov-config=.coveragerc --cov-report=term-missing --cov-report=xml --cov-report=html
49
+ pytest-clean:
50
+ rm -rf .pytest_cache
51
+ test: test-prepare flake8 pylint pytest
52
+ test-clean: pytest-clean
53
+
54
+
55
+ clean-cover:
56
+ rm -rf cover .coverage coverage.xml htmlcov
57
+ clean-tox:
58
+ rm -rf .stestr .tox
59
+ clean: build-clean test-clean clean-cover clean-tox
@@ -0,0 +1,9 @@
1
+ # flake8: noqa: E402
2
+
3
+ from hatchling.metadata.plugin.interface import MetadataHookInterface
4
+
5
+
6
+ class CustomMetadataHook(MetadataHookInterface): # pylint: disable=R0903
7
+
8
+ def update(self, metadata: dict) -> None:
9
+ pass
@@ -0,0 +1,48 @@
1
+ [build-system]
2
+ build-backend = "hatchling.build"
3
+ requires = [
4
+ "hatch-requirements-txt",
5
+ "hatchling",
6
+ "xpip-build>=1.4",
7
+ ]
8
+
9
+ [project]
10
+ authors = []
11
+ classifiers = [
12
+ "Programming Language :: Python",
13
+ "Programming Language :: Python :: 3",
14
+ ]
15
+ description = ""
16
+ dynamic = [
17
+ "dependencies",
18
+ "version",
19
+ ]
20
+ keywords = []
21
+ license-files = [ "LICENSE",]
22
+ name = ""
23
+ requires-python = ""
24
+
25
+ [project.readme]
26
+ content-type = "text/markdown"
27
+ file = "readme.md"
28
+
29
+ [project.scripts]
30
+
31
+ [project.urls]
32
+
33
+ [tool.hatch.version]
34
+
35
+ [tool.hatch.build.targets.sdist]
36
+ exclude = []
37
+ packages = []
38
+
39
+ [tool.hatch.build.targets.wheel]
40
+ exclude = []
41
+ packages = []
42
+
43
+ [tool.hatch.metadata.hooks.requirements_txt]
44
+ files = []
45
+
46
+ [tool.hatch.build.targets.sdist.force-include]
47
+
48
+ [tool.hatch.build.targets.wheel.force-include]
@@ -0,0 +1,3 @@
1
+ # {project_name}
2
+
3
+ > {project_description}
@@ -0,0 +1,216 @@
1
+ # coding:utf-8
2
+
3
+ from configparser import ConfigParser
4
+ from pathlib import Path
5
+ from typing import Any
6
+ from typing import Dict
7
+ from typing import Iterator
8
+ from typing import List
9
+ from typing import Optional
10
+ from typing import Type
11
+ from typing import TypeVar
12
+ from typing import Union
13
+
14
+ from packaging.requirements import Requirement
15
+ from toml import dump
16
+ from toml import load
17
+
18
+
19
+ CFGT = TypeVar("CFGT", bound="Configuration")
20
+
21
+
22
+ class Configuration:
23
+
24
+ def __init__(self, parser: Optional[ConfigParser] = None):
25
+ self.__parser: ConfigParser = parser or ConfigParser()
26
+
27
+ @property
28
+ def parser(self) -> ConfigParser:
29
+ return self.__parser
30
+
31
+ def dump(self, filepath: Union[str, Path], writable: bool = False):
32
+ if isinstance(filepath, str):
33
+ filepath = Path(filepath) # pragma: no cover
34
+
35
+ if not filepath.exists() or writable:
36
+ with filepath.open("w", encoding="utf-8") as whdl:
37
+ self.parser.write(whdl)
38
+
39
+ @classmethod
40
+ def load(cls: Type[CFGT], filepath: Union[str, Path]) -> CFGT:
41
+ (parser := ConfigParser()).read(filepath)
42
+ return cls(parser=parser)
43
+
44
+
45
+ class CoverageRC(Configuration):
46
+ FILENAME: str = ".coveragerc"
47
+
48
+
49
+ class Flake8(Configuration):
50
+ FILENAME: str = ".flake8"
51
+
52
+
53
+ class PylintRC(Configuration):
54
+ FILENAME: str = ".pylintrc"
55
+
56
+
57
+ class Pyproject: # pylint: disable=too-many-public-methods
58
+ FILENAME: str = "pyproject.toml"
59
+
60
+ def __init__(self, coder: Dict[str, Any]):
61
+ self.__coder: Dict[str, Any] = coder
62
+
63
+ @property
64
+ def coder(self) -> Dict[str, Any]:
65
+ return self.__coder
66
+
67
+ @property
68
+ def project(self) -> Dict[str, Any]:
69
+ return self.coder["project"]
70
+
71
+ @property
72
+ def project_authors(self) -> List[Dict[str, str]]:
73
+ return self.project["authors"]
74
+
75
+ @property
76
+ def project_keywords(self) -> List[str]:
77
+ return self.project["keywords"]
78
+
79
+ @property
80
+ def project_scripts(self) -> Dict[str, Any]:
81
+ return self.project["scripts"]
82
+
83
+ @property
84
+ def project_urls(self) -> Dict[str, str]:
85
+ return self.project["urls"]
86
+
87
+ @property
88
+ def tool(self) -> Dict[str, Any]:
89
+ return self.coder["tool"]
90
+
91
+ @property
92
+ def tool_hatch(self) -> Dict[str, Any]:
93
+ return self.tool["hatch"]
94
+
95
+ @property
96
+ def tool_hatch_build(self) -> Dict[str, Any]:
97
+ return self.tool_hatch["build"]
98
+
99
+ @property
100
+ def tool_hatch_build_targets(self) -> Dict[str, Any]:
101
+ return self.tool_hatch_build["targets"]
102
+
103
+ @property
104
+ def tool_hatch_build_targets_sdist(self) -> Dict[str, Any]:
105
+ return self.tool_hatch_build_targets["sdist"]
106
+
107
+ @property
108
+ def tool_hatch_build_targets_sdist_force_include(self) -> Dict[str, Any]:
109
+ return self.tool_hatch_build_targets_sdist["force-include"]
110
+
111
+ @property
112
+ def tool_hatch_build_targets_sdist_exclude(self) -> List[str]:
113
+ return self.tool_hatch_build_targets_sdist["exclude"]
114
+
115
+ @property
116
+ def tool_hatch_build_targets_sdist_packages(self) -> List[str]:
117
+ return self.tool_hatch_build_targets_sdist["packages"]
118
+
119
+ @property
120
+ def tool_hatch_build_targets_wheel(self) -> Dict[str, Any]:
121
+ return self.tool_hatch_build_targets["wheel"]
122
+
123
+ @property
124
+ def tool_hatch_build_targets_wheel_force_include(self) -> Dict[str, Any]:
125
+ return self.tool_hatch_build_targets_wheel["force-include"]
126
+
127
+ @property
128
+ def tool_hatch_build_targets_wheel_exclude(self) -> List[str]:
129
+ return self.tool_hatch_build_targets_wheel["exclude"]
130
+
131
+ @property
132
+ def tool_hatch_build_targets_wheel_packages(self) -> List[str]:
133
+ return self.tool_hatch_build_targets_wheel["packages"]
134
+
135
+ @property
136
+ def tool_hatch_metadata(self) -> Dict[str, Any]:
137
+ return self.tool_hatch["metadata"]
138
+
139
+ @property
140
+ def tool_hatch_metadata_hooks(self) -> Dict[str, Any]:
141
+ return self.tool_hatch_metadata["hooks"]
142
+
143
+ @property
144
+ def tool_hatch_metadata_hooks_requirements_txt(self) -> Dict[str, Any]:
145
+ return self.tool_hatch_metadata_hooks["requirements_txt"]
146
+
147
+ @property
148
+ def tool_hatch_metadata_hooks_requirements_txt_files(self) -> List[str]:
149
+ return self.tool_hatch_metadata_hooks_requirements_txt["files"]
150
+
151
+ @property
152
+ def tool_hatch_version(self) -> List[str]:
153
+ return self.tool_hatch["version"]
154
+
155
+ def dump(self, filepath: Union[str, Path], writable: bool = False):
156
+ if isinstance(filepath, str):
157
+ filepath = Path(filepath) # pragma: no cover
158
+
159
+ if not filepath.exists() or writable:
160
+ with filepath.open("w", encoding="utf-8") as whdl:
161
+ dump(self.coder, whdl)
162
+
163
+ @classmethod
164
+ def load(cls, filepath: Union[str, Path]) -> "Pyproject":
165
+ if isinstance(filepath, str):
166
+ filepath = Path(filepath) # pragma: no cover
167
+
168
+ with filepath.open("r", encoding="utf-8") as rhdl:
169
+ return cls(coder=load(rhdl))
170
+
171
+
172
+ class Requirements:
173
+ """Requirements
174
+
175
+ Reference:
176
+ - [PEP 508](https://peps.python.org/pep-0508/)
177
+ Dependency specification for Python Software Packages
178
+ """
179
+ FILENAME: str = "requirements.txt"
180
+
181
+ def __init__(self, *requirements: Union[str, Requirement]):
182
+ self.__requirements: List[Requirement] = [self.normalize(requirement) for requirement in requirements] # noqa:E501
183
+
184
+ def __iter__(self) -> Iterator[Requirement]:
185
+ yield from self.__requirements
186
+
187
+ def add(self, requirement: Union[str, Requirement]) -> None:
188
+ self.__requirements.append(self.normalize(requirement))
189
+
190
+ def dumps(self) -> str:
191
+ return "\n".join(str(requirement) for requirement in self.__requirements) # noqa:E501
192
+
193
+ def dumpf(self, filepath: Union[str, Path], writable: bool = False) -> None: # noqa:E501
194
+ if isinstance(filepath, str):
195
+ filepath = Path(filepath) # pragma: no cover
196
+
197
+ if not filepath.exists() or writable:
198
+ with filepath.open("w", encoding="utf-8") as whdl:
199
+ whdl.write(self.dumps())
200
+ whdl.write("\n")
201
+
202
+ @classmethod
203
+ def normalize(cls, requirement: Union[str, Requirement]) -> Requirement: # noqa:E501
204
+ """Normalized Names with PEP 503
205
+
206
+ Reference:
207
+ - https://peps.python.org/pep-0426/#name
208
+ - https://peps.python.org/pep-0503/#normalized-names
209
+ """
210
+ from re import sub # pylint: disable=import-outside-toplevel
211
+
212
+ if not isinstance(requirement, Requirement):
213
+ requirement = Requirement(requirement)
214
+
215
+ requirement.name = sub(r"[-_.]+", "-", requirement.name).lower()
216
+ return requirement