From a7537e0bd5a596e8e5df4e6c946f7c3e5bf31d66 Mon Sep 17 00:00:00 2001 From: phitoduck Date: Sat, 28 Sep 2024 04:09:59 -0600 Subject: [PATCH] lint: disabled linting and type checking --- .github/workflows/build-test-publish.yaml | 2 +- .pre-commit-config.yaml | 52 +++++++++--------- .vscode/extensions.json | 2 +- .vscode/settings.json | 2 +- README.md | 36 ++++++------- pyproject.toml | 2 +- src/pyprojen/_resolve.py | 16 ++++-- src/pyprojen/cleanup.py | 17 +++--- src/pyprojen/common.py | 2 +- src/pyprojen/component.py | 17 ++++-- src/pyprojen/constructs/__init__.py | 14 ++++- src/pyprojen/constructs/construct.py | 64 ++++++++++++++-------- src/pyprojen/constructs/dependency.py | 29 ++++++---- src/pyprojen/file.py | 30 ++++++++--- src/pyprojen/ignore_file.py | 19 +++++-- src/pyprojen/json_file.py | 12 +++-- src/pyprojen/json_patch.py | 27 ++++++---- src/pyprojen/object_file.py | 33 +++++++----- src/pyprojen/project.py | 31 +++++------ src/pyprojen/textfile.py | 19 ++++--- src/pyprojen/toml_file.py | 11 ++-- src/pyprojen/util/__init__.py | 36 ++++++------- src/pyprojen/util/constructs.py | 39 ++++++++++---- src/pyprojen/util/name.py | 11 +++- src/pyprojen/util/object.py | 8 ++- src/pyprojen/util/path.py | 10 ++-- src/pyprojen/util/semver.py | 61 +++++++++++---------- src/pyprojen/util/synth.py | 66 ++++++++++++++--------- src/pyprojen/util/tasks.py | 12 +++-- src/pyprojen/util/util.py | 5 +- src/pyprojen/yaml_file.py | 14 +++-- tests/unit_tests/test__example.py | 3 +- 32 files changed, 433 insertions(+), 269 deletions(-) diff --git a/.github/workflows/build-test-publish.yaml b/.github/workflows/build-test-publish.yaml index 75d78b4..4351761 100644 --- a/.github/workflows/build-test-publish.yaml +++ b/.github/workflows/build-test-publish.yaml @@ -18,7 +18,7 @@ permissions: contents: write jobs: - + check-version-txt: runs-on: ubuntu-latest steps: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 74889a7..2fca94a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -46,16 +46,16 @@ repos: # Detects the presence of private keys - id: detect-private-key - - repo: https://github.com/pre-commit/mirrors-mypy - rev: "v1.2.0" - hooks: - - id: mypy - args: - [ - --no-strict-optional, - --ignore-missing-imports, - --config-file=./pyproject.toml, - ] + # - repo: https://github.com/pre-commit/mirrors-mypy + # rev: "v1.2.0" + # hooks: + # - id: mypy + # args: + # [ + # --no-strict-optional, + # --ignore-missing-imports, + # --config-file=./pyproject.toml, + # ] - repo: https://github.com/psf/black rev: 23.1.0 @@ -64,23 +64,23 @@ repos: args: - --config=./pyproject.toml - - repo: https://github.com/PyCQA/pylint - rev: v2.16.3 - hooks: - - id: pylint - args: - - --rcfile=./pyproject.toml + # - repo: https://github.com/PyCQA/pylint + # rev: v2.16.3 + # hooks: + # - id: pylint + # args: + # - --rcfile=./pyproject.toml - - repo: https://github.com/PyCQA/flake8 - rev: 6.0.0 - hooks: - - id: flake8 - args: - - --toml-config=./pyproject.toml - additional_dependencies: - - radon - - flake8-docstrings - - Flake8-pyproject + # - repo: https://github.com/PyCQA/flake8 + # rev: 6.0.0 + # hooks: + # - id: flake8 + # args: + # - --toml-config=./pyproject.toml + # additional_dependencies: + # - radon + # - flake8-docstrings + # - Flake8-pyproject - repo: https://github.com/pycqa/isort rev: 5.12.0 diff --git a/.vscode/extensions.json b/.vscode/extensions.json index f2b89c9..254094d 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -36,4 +36,4 @@ // jump to definition of pytest fixtures, e.g. with Ctrl+Click / Cmd+Click "nickmillerdev.pytest-fixtures", ] -} \ No newline at end of file +} diff --git a/.vscode/settings.json b/.vscode/settings.json index d4f3a0b..2d54888 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -46,4 +46,4 @@ "color": "green" } ], -} \ No newline at end of file +} diff --git a/README.md b/README.md index 0f13edd..de969d7 100644 --- a/README.md +++ b/README.md @@ -37,33 +37,33 @@ Project templates are great... except that they promote "code duplication at sca ## Real Examples -`pyprojen` is a minimal port of `projen`'s core functionality to Python. +`pyprojen` is a minimal port of `projen`'s core functionality to Python. Specifically, it gets you 1. ⚡️ **Agility** - - - **Examples:** + + - **Examples:** - 10x the speed of CI for all repos overnight by making CI steps run in parallel instead of serially - Update a team linting rule in `pyproject.toml`, `ruff.toml`, etc. - Add auth steps to publish to or install from a private PyPI server in CI or `Dockerfile`s - Completely change CI systems with minimal disruption, e.g. switch from Bitbucket Pipelines to GitHub Actions to AWS CodeBuild and back 2. 🧱 **Modularity** - - - **Examples**: - 1. define an opinionated `PythonPackage` component, and - 1. layer on top a - - `FastAPIApp`, - - `StreamlitApp`, - - `CdkApp`, - - `PulumiApp`, - - `AirflowDag`, - - `DagsterDag`, + + - **Examples**: + 1. define an opinionated `PythonPackage` component, and + 1. layer on top a + - `FastAPIApp`, + - `StreamlitApp`, + - `CdkApp`, + - `PulumiApp`, + - `AirflowDag`, + - `DagsterDag`, - `BentoMLService`, - `AwsLambdaPythonFunction`, - etc. - 1. Add (or remove) as many of these to your repo as you like, whenever you like, and find these packages instantly set up with CI, linting, formatting, tests, packaging, publishing, deploying, etc. + 1. Add (or remove) as many of these to your repo as you like, whenever you like, and find these packages instantly set up with CI, linting, formatting, tests, packaging, publishing, deploying, etc. - For example, you might incrementally develop a "mini data science app monorepo" with 1. a `MetaflowDag` that trains a model 1. served in a `FastAPI` app @@ -100,7 +100,7 @@ That said, although `cookiecutter` and `copier` are more limited, they are also 4. An opinionated "task runner" system (think `Makefile/Justfile`, `poetry` scripts, etc.) to define project-related commands. 5. A `projen new` command which creates the initial `.projenrc.py` config file for your project -#### 2. `pyprojen` implements [1] and [2] from the list above (the unopinionated parts). +#### 2. `pyprojen` implements [1] and [2] from the list above (the unopinionated parts). It is up to you to create your own components with your own opinions on things like @@ -127,7 +127,7 @@ If you write components in Python using `pyprojen`, it should be easy to move th `projen` is a larger project and is primarily maintained by developers at AWS. `projen`, -But to develop with `projen`, you either need to write TypeScript, or use generated Python bindings that invoke TypeScript. +But to develop with `projen`, you either need to write TypeScript, or use generated Python bindings that invoke TypeScript. If you are familiar with writing AWS CDK in Python, developing with `projen` in Python is a similar experience, because they both use Python bindings generated from TypeScript using the [JSII](https://github.com/aws/jsii) project. @@ -143,7 +143,7 @@ This means: ## Quick start (TODO) -> [!NOTE] +> [!NOTE] > Until this section is filled out, you can refer to [this repo](https://github.com/phitoduck/phito-projen) to get a sense of what projen can do. And the official [projen docs](https://projen.io/) contain many of the same concepts that this port uses. ```bash @@ -164,7 +164,7 @@ You will need the following installed on your machine to develop on this codebas - Python 3.7+, ideally using `pyenv` to easily change between Python versions - `git` -### +### ```bash # clone the repo diff --git a/pyproject.toml b/pyproject.toml index 7d3e6b5..92bbca4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ static-code-qa = [ "Flake8-pyproject", "radon", ] -# Installing dev depenendencies in your virtual env makes it so that during development VS Code can +# Installing dev depenendencies in your virtual env makes it so that during development VS Code can # - surface linting errors # - automatically apply formatting # - show enhanced autocompletion for stubs libraries diff --git a/src/pyprojen/_resolve.py b/src/pyprojen/_resolve.py index 4476499..ab8fd0e 100644 --- a/src/pyprojen/_resolve.py +++ b/src/pyprojen/_resolve.py @@ -1,12 +1,18 @@ -from typing import Any, Dict, List import re +from typing import ( + Any, + Dict, +) + class IResolvable: def to_json(self) -> Any: raise NotImplementedError() + def is_resolvable(obj: Any) -> bool: - return hasattr(obj, 'to_json') and callable(getattr(obj, 'to_json')) + return hasattr(obj, "to_json") and callable(getattr(obj, "to_json")) + def resolve(value: Any, options: Dict[str, Any] = {}) -> Any: """ @@ -24,8 +30,8 @@ def resolve(value: Any, options: Dict[str, Any] = {}) -> Any: Raises: ValueError: If a regular expression with flags is encountered. """ - args = options.get('args', []) - omit_empty = options.get('omit_empty', False) + args = options.get("args", []) + omit_empty = options.get("omit_empty", False) match value: case None: @@ -51,4 +57,4 @@ def resolve(value: Any, options: Dict[str, Any] = {}) -> Any: case _ if callable(value): return resolve(value(*args), options) case _: - return value \ No newline at end of file + return value diff --git a/src/pyprojen/cleanup.py b/src/pyprojen/cleanup.py index 5c6b111..3e9aee3 100644 --- a/src/pyprojen/cleanup.py +++ b/src/pyprojen/cleanup.py @@ -1,10 +1,11 @@ -import os import json -from typing import List import logging +import os +from typing import List FILE_MANIFEST = ".pyprojen/files.json" + def cleanup(dir: str, new_files: List[str], exclude: List[str]): try: manifest_files = get_files_from_manifest(dir) @@ -17,6 +18,7 @@ def cleanup(dir: str, new_files: List[str], exclude: List[str]): except Exception as e: logging.warn(f"warning: failed to clean up generated files: {str(e)}") + def remove_files(files: List[str]): for file in files: try: @@ -24,22 +26,25 @@ def remove_files(files: List[str]): except Exception as e: logging.warn(f"Failed to remove file {file}: {str(e)}") + def find_orphaned_files(dir: str, old_files: List[str], new_files: List[str]) -> List[str]: return [os.path.join(dir, f) for f in old_files if f not in new_files] + def find_generated_files(dir: str, exclude: List[str]) -> List[str]: # Implement this function to find generated files based on a marker # This is a placeholder and should be implemented based on your specific needs return [] + def get_files_from_manifest(dir: str) -> List[str]: try: manifest_path = os.path.join(dir, FILE_MANIFEST) if os.path.exists(manifest_path): - with open(manifest_path, 'r') as f: + with open(manifest_path, "r") as f: manifest = json.load(f) - if 'files' in manifest: - return manifest['files'] + if "files" in manifest: + return manifest["files"] except Exception as e: logging.warn(f"warning: unable to get files to clean from file manifest: {str(e)}") - return [] \ No newline at end of file + return [] diff --git a/src/pyprojen/common.py b/src/pyprojen/common.py index 371e233..ab2a25e 100644 --- a/src/pyprojen/common.py +++ b/src/pyprojen/common.py @@ -2,4 +2,4 @@ Common constants and utilities for pyprojen. """ -FILE_MANIFEST = ".pyprojen/files.json" \ No newline at end of file +FILE_MANIFEST = ".pyprojen/files.json" diff --git a/src/pyprojen/component.py b/src/pyprojen/component.py index f294f48..a38a1fb 100644 --- a/src/pyprojen/component.py +++ b/src/pyprojen/component.py @@ -1,6 +1,16 @@ -from typing import Any, Dict, Optional +from typing import ( + Any, + Dict, + Optional, +) + from pyprojen.constructs import Construct -from pyprojen.util.constructs import is_component, find_closest_project, tag_as_component +from pyprojen.util.constructs import ( + find_closest_project, + is_component, + tag_as_component, +) + class Component(Construct): """ @@ -48,16 +58,13 @@ def pre_synthesize(self): """ Called before synthesis. """ - pass def synthesize(self): """ Synthesizes files to the project output directory. """ - pass def post_synthesize(self): """ Called after synthesis. """ - pass \ No newline at end of file diff --git a/src/pyprojen/constructs/__init__.py b/src/pyprojen/constructs/__init__.py index 7c35e29..58a185f 100644 --- a/src/pyprojen/constructs/__init__.py +++ b/src/pyprojen/constructs/__init__.py @@ -1,5 +1,15 @@ -from .construct import Construct, IConstruct, Node, ConstructOrder, MetadataEntry -from .dependency import IDependable, DependencyGroup, Dependable +from .construct import ( + Construct, + ConstructOrder, + IConstruct, + MetadataEntry, + Node, +) +from .dependency import ( + Dependable, + DependencyGroup, + IDependable, +) __all__ = [ "Construct", diff --git a/src/pyprojen/constructs/construct.py b/src/pyprojen/constructs/construct.py index 4f31fc3..d0e1d46 100644 --- a/src/pyprojen/constructs/construct.py +++ b/src/pyprojen/constructs/construct.py @@ -1,21 +1,35 @@ +import hashlib import re +from abc import ( + ABC, + abstractmethod, +) from enum import Enum -from typing import List, Optional, Dict, Any, Union, Set -from abc import ABC, abstractmethod -import hashlib +from typing import ( + Any, + Dict, + List, + Optional, + Set, + Union, +) + +from .dependency import ( + Dependable, + IDependable, +) -from .dependency import IDependable, Dependable +CONSTRUCT_SYM = "constructs.Construct" -CONSTRUCT_SYM = 'constructs.Construct' class IConstruct(IDependable): """Interface for constructs.""" @property @abstractmethod - def node(self) -> 'Node': + def node(self) -> "Node": """The tree node.""" - pass + class ConstructOrder(Enum): """Order in which to return constructs.""" @@ -23,6 +37,7 @@ class ConstructOrder(Enum): PREORDER = 1 POSTORDER = 2 + class MetadataEntry: """Represents a metadata entry.""" @@ -38,6 +53,7 @@ def __init__(self, type: str, data: Any, trace: Optional[List[str]] = None): self.data = data self.trace = trace + class IValidation(ABC): """Interface for validation.""" @@ -48,15 +64,15 @@ def validate(self) -> List[str]: :return: List of error messages """ - pass + class Node: """Represents the construct node in the scope tree.""" - PATH_SEP = '/' + PATH_SEP = "/" @staticmethod - def of(construct: IConstruct) -> 'Node': + def of(construct: IConstruct) -> "Node": """ Returns the node associated with a construct. @@ -65,7 +81,7 @@ def of(construct: IConstruct) -> 'Node': """ return construct.node - def __init__(self, host: 'Construct', scope: Optional[IConstruct], id: str): + def __init__(self, host: "Construct", scope: Optional[IConstruct], id: str): """ Initialize a Node. @@ -75,7 +91,7 @@ def __init__(self, host: 'Construct', scope: Optional[IConstruct], id: str): """ self._host = host self.scope = scope - self.id = self._sanitize_id(id or '') + self.id = self._sanitize_id(id or "") self._children: Dict[str, IConstruct] = {} self._context: Dict[str, Any] = {} self._metadata: List[MetadataEntry] = [] @@ -162,7 +178,7 @@ def validate(self) -> List[str]: """ return [error for validation in self._validations for error in validation.validate()] - def _add_child(self, child: 'Construct', child_name: str): + def _add_child(self, child: "Construct", child_name: str): """ Add a child construct. @@ -172,7 +188,9 @@ def _add_child(self, child: 'Construct', child_name: str): if self._locked: raise ValueError(f"Cannot add children to {self.path} during synthesis") if child_name in self._children: - raise ValueError(f"There is already a Construct with name '{child_name}' in {self._host.__class__.__name__}") + raise ValueError( + f"There is already a Construct with name '{child_name}' in {self._host.__class__.__name__}" + ) self._children[child_name] = child @staticmethod @@ -183,7 +201,7 @@ def _sanitize_id(id: str) -> str: :param id: The ID to sanitize :return: The sanitized ID """ - return re.sub(f'{Node.PATH_SEP}', '--', id) + return re.sub(f"{Node.PATH_SEP}", "--", id) @property def addr(self) -> str: @@ -205,10 +223,10 @@ def _calculate_addr(self) -> str: components = [c.node.id for c in self.scopes] hash_object = hashlib.sha1() for c in components: - if c != 'Default': + if c != "Default": hash_object.update(c.encode()) - hash_object.update(b'\n') - return 'c8' + hash_object.hexdigest() + hash_object.update(b"\n") + return "c8" + hash_object.hexdigest() def find_all(self) -> List[IConstruct]: """ @@ -224,16 +242,17 @@ def find_all(self) -> List[IConstruct]: def try_find_child(self, id: str) -> Optional[IConstruct]: """ Attempts to find a child construct by its id. - + :param id: The id of the child construct to find :return: The child construct if found, None otherwise """ return self._children.get(id) + class Construct(IConstruct): """Represents a construct.""" - def __init__(self, scope: Optional[Union['Construct', IConstruct]], id: str): + def __init__(self, scope: Optional[Union["Construct", IConstruct]], id: str): """ Initialize a Construct. @@ -268,7 +287,8 @@ def __str__(self): :return: The string representation """ - return self.node.path or '' + return self.node.path or "" + # Mark all instances of 'Construct' -setattr(Construct, CONSTRUCT_SYM, True) \ No newline at end of file +setattr(Construct, CONSTRUCT_SYM, True) diff --git a/src/pyprojen/constructs/dependency.py b/src/pyprojen/constructs/dependency.py index 9983a1c..3125be4 100644 --- a/src/pyprojen/constructs/dependency.py +++ b/src/pyprojen/constructs/dependency.py @@ -1,6 +1,11 @@ -from abc import ABC, abstractmethod -from typing import List, Any, Set, TYPE_CHECKING - +from abc import ( + ABC, + abstractmethod, +) +from typing import ( + TYPE_CHECKING, + List, +) if TYPE_CHECKING: from pyprojen.constructs.construct import IConstruct @@ -9,36 +14,38 @@ class IDependable(ABC): pass + class Dependable: @staticmethod - def implement(instance: IDependable, trait: 'Dependable') -> None: - setattr(instance, '_dependable_trait', trait) + def implement(instance: IDependable, trait: "Dependable") -> None: + setattr(instance, "_dependable_trait", trait) @staticmethod - def of(instance: IDependable) -> 'Dependable': - trait = getattr(instance, '_dependable_trait', None) + def of(instance: IDependable) -> "Dependable": + trait = getattr(instance, "_dependable_trait", None) if trait is None: raise ValueError(f"{instance} does not implement IDependable. Use 'Dependable.implement()' to implement") return trait @property @abstractmethod - def dependency_roots(self) -> List['IConstruct']: + def dependency_roots(self) -> List["IConstruct"]: pass + class DependencyGroup(IDependable): def __init__(self, *deps: IDependable): self._deps: List[IDependable] = [] - + Dependable.implement(self, self) self.add(*deps) @property - def dependency_roots(self) -> List['IConstruct']: + def dependency_roots(self) -> List["IConstruct"]: result = [] for d in self._deps: result.extend(Dependable.of(d).dependency_roots) return result def add(self, *scopes: IDependable) -> None: - self._deps.extend(scopes) \ No newline at end of file + self._deps.extend(scopes) diff --git a/src/pyprojen/file.py b/src/pyprojen/file.py index 0a49bc2..f608f7c 100644 --- a/src/pyprojen/file.py +++ b/src/pyprojen/file.py @@ -1,12 +1,25 @@ import os import shutil -from abc import ABC, abstractmethod -from typing import Any, Dict, Optional +from abc import ( + ABC, + abstractmethod, +) +from typing import ( + Any, + Dict, + Optional, +) from pyprojen.component import Component -from pyprojen.util import is_writable, normalize_persisted_path, try_read_file_sync, write_file +from pyprojen.util import ( + is_writable, + normalize_persisted_path, + try_read_file_sync, + write_file, +) from pyprojen.util.constructs import find_closest_project + class FileBase(Component, ABC): """ Base class for files in the project. @@ -56,7 +69,11 @@ def __init__( self._should_add_marker = marker if marker is not None else True glob_pattern = self.path - committed = committed if committed is not None else (project.commit_generated if project.commit_generated is not None else True) + committed = ( + committed + if committed is not None + else (project.commit_generated if project.commit_generated is not None else True) + ) if committed and file_path != ".gitattributes": project.annotate_generated(file_path) @@ -92,7 +109,6 @@ def synthesize_content(self, resolver: "IResolver") -> Optional[str]: :param resolver: The resolver to use :return: The synthesized content or None """ - pass def synthesize(self): """ @@ -126,6 +142,7 @@ def changed(self) -> Optional[bool]: """ return self._changed + class IResolver: """ Interface for resolving values. @@ -141,6 +158,7 @@ def resolve(self, value: Any, options: Optional[Dict[str, Any]] = None) -> Any: """ return value + class ResolveOptions: """ Options for resolving values. @@ -156,6 +174,7 @@ def __init__(self, omit_empty: bool = False, args: Optional[list] = None): self.omit_empty = omit_empty self.args = args or [] + class IResolvable(ABC): """ Interface for resolvable objects. @@ -168,4 +187,3 @@ def to_json(self) -> Any: :return: The JSON representation of the object """ - pass diff --git a/src/pyprojen/ignore_file.py b/src/pyprojen/ignore_file.py index d795585..c292d37 100644 --- a/src/pyprojen/ignore_file.py +++ b/src/pyprojen/ignore_file.py @@ -1,18 +1,27 @@ -from typing import List, Optional, TYPE_CHECKING -from pyprojen.file import FileBase, IResolver +from typing import ( + TYPE_CHECKING, + List, + Optional, +) + +from pyprojen.file import ( + FileBase, + IResolver, +) from pyprojen.util import normalize_persisted_path if TYPE_CHECKING: from pyprojen.project import Project + class IgnoreFile(FileBase): def __init__( self, - project: 'Project', + project: "Project", file_path: str, filter_comment_lines: bool = True, filter_empty_lines: bool = True, - ignore_patterns: Optional[List[str]] = None + ignore_patterns: Optional[List[str]] = None, ): super().__init__(project, file_path, edit_gitignore=False) self.filter_comment_lines = filter_comment_lines @@ -67,4 +76,4 @@ def _remove(self, value: str): try: self._patterns.remove(value) except ValueError: - pass \ No newline at end of file + pass diff --git a/src/pyprojen/json_file.py b/src/pyprojen/json_file.py index 956e062..4552477 100644 --- a/src/pyprojen/json_file.py +++ b/src/pyprojen/json_file.py @@ -1,8 +1,12 @@ import json -from typing import Any, Optional +from typing import ( + Any, + Optional, +) from pyprojen.object_file import ObjectFile + class JsonFile(ObjectFile): def __init__( self, @@ -18,7 +22,9 @@ def __init__( ): super().__init__(scope, file_path, obj, omit_empty, committed=committed, readonly=readonly) self.newline = newline - self.supports_comments = allow_comments if allow_comments is not None else file_path.lower().endswith(('json5', 'jsonc')) + self.supports_comments = ( + allow_comments if allow_comments is not None else file_path.lower().endswith(("json5", "jsonc")) + ) def synthesize_content(self, resolver: Any) -> Optional[str]: content = super().synthesize_content(resolver) @@ -40,4 +46,4 @@ def serialize(self, obj: Any) -> str: content = json.dumps(obj, indent=2) if self.newline: content += "\n" - return content \ No newline at end of file + return content diff --git a/src/pyprojen/json_patch.py b/src/pyprojen/json_patch.py index 5ef29a6..746fd25 100644 --- a/src/pyprojen/json_patch.py +++ b/src/pyprojen/json_patch.py @@ -1,7 +1,11 @@ -from typing import Any, List, Dict -import jsonpatch -import json from enum import Enum +from typing import ( + Any, + Dict, +) + +import jsonpatch + class JsonPatchOperation(Enum): """Enum for JSON Patch operations.""" @@ -13,6 +17,7 @@ class JsonPatchOperation(Enum): COPY = "copy" TEST = "test" + class JsonPatch: """Represents a JSON Patch operation.""" @@ -31,7 +36,7 @@ def __init__(self, operation: str, path: str, value: Any = None, from_: str = No self.from_ = from_ @staticmethod - def add(path: str, value: Any) -> 'JsonPatch': + def add(path: str, value: Any) -> "JsonPatch": """ Create an 'add' JSON Patch operation. @@ -42,7 +47,7 @@ def add(path: str, value: Any) -> 'JsonPatch': return JsonPatch("add", path, value) @staticmethod - def remove(path: str) -> 'JsonPatch': + def remove(path: str) -> "JsonPatch": """ Create a 'remove' JSON Patch operation. @@ -52,7 +57,7 @@ def remove(path: str) -> 'JsonPatch': return JsonPatch("remove", path) @staticmethod - def replace(path: str, value: Any) -> 'JsonPatch': + def replace(path: str, value: Any) -> "JsonPatch": """ Create a 'replace' JSON Patch operation. @@ -63,7 +68,7 @@ def replace(path: str, value: Any) -> 'JsonPatch': return JsonPatch("replace", path, value) @staticmethod - def move(from_: str, path: str) -> 'JsonPatch': + def move(from_: str, path: str) -> "JsonPatch": """ Create a 'move' JSON Patch operation. @@ -74,7 +79,7 @@ def move(from_: str, path: str) -> 'JsonPatch': return JsonPatch("move", path, from_=from_) @staticmethod - def copy(from_: str, path: str) -> 'JsonPatch': + def copy(from_: str, path: str) -> "JsonPatch": """ Create a 'copy' JSON Patch operation. @@ -85,7 +90,7 @@ def copy(from_: str, path: str) -> 'JsonPatch': return JsonPatch("copy", path, from_=from_) @staticmethod - def test(path: str, value: Any) -> 'JsonPatch': + def test(path: str, value: Any) -> "JsonPatch": """ Create a 'test' JSON Patch operation. @@ -109,7 +114,7 @@ def to_dict(self) -> Dict[str, Any]: return patch_dict @staticmethod - def apply(obj: Any, *patches: 'JsonPatch') -> Any: + def apply(obj: Any, *patches: "JsonPatch") -> Any: """ Apply JSON Patch operations to an object. @@ -118,4 +123,4 @@ def apply(obj: Any, *patches: 'JsonPatch') -> Any: :return: The patched object """ patch_list = [patch.to_dict() for patch in patches] - return jsonpatch.apply_patch(obj, patch_list) \ No newline at end of file + return jsonpatch.apply_patch(obj, patch_list) diff --git a/src/pyprojen/object_file.py b/src/pyprojen/object_file.py index 5cc8a5a..2661f22 100644 --- a/src/pyprojen/object_file.py +++ b/src/pyprojen/object_file.py @@ -1,9 +1,18 @@ from abc import ABC -from typing import Any, Optional, List -from pyprojen.file import FileBase, IResolver +from typing import ( + Any, + List, + Optional, +) + +from pyprojen._resolve import resolve +from pyprojen.file import ( + FileBase, + IResolver, +) from pyprojen.json_patch import JsonPatch from pyprojen.util import deep_merge -from pyprojen._resolve import resolve + class ObjectFile(FileBase, ABC): """ @@ -35,16 +44,16 @@ def synthesize_content(self, resolver: IResolver) -> Optional[str]: """ obj = self._obj() if callable(self._obj) else self._obj resolved = resolve(obj, {"omit_empty": self._omit_empty}) - + if resolved is None: return None - + deep_merge([resolved, self._raw_overrides], True) - + patched = resolved for patch in self._patch_operations: patched = JsonPatch.apply(patched, patch) - + return self.serialize(patched) if patched else None def serialize(self, obj: Any) -> str: @@ -98,13 +107,13 @@ def _split_on_periods(x: str) -> List[str]: :param x: The string to split :return: A list of split string parts """ - ret = [''] + ret = [""] for i, char in enumerate(x): - if char == '\\' and i + 1 < len(x): + if char == "\\" and i + 1 < len(x): ret[-1] += x[i + 1] i += 1 - elif char == '.': - ret.append('') + elif char == ".": + ret.append("") else: ret[-1] += char return [part for part in ret if part] @@ -123,4 +132,4 @@ def set_object(self, obj: Any): :param obj: The new object to set """ - self._obj = obj \ No newline at end of file + self._obj = obj diff --git a/src/pyprojen/project.py b/src/pyprojen/project.py index f9d1601..3ebf721 100644 --- a/src/pyprojen/project.py +++ b/src/pyprojen/project.py @@ -1,8 +1,5 @@ import os -from abc import ( - ABC, - abstractmethod, -) +from abc import ABC from typing import ( Any, Dict, @@ -10,20 +7,18 @@ Optional, ) +from pyprojen.cleanup import ( + FILE_MANIFEST, + cleanup, +) +from pyprojen.common import FILE_MANIFEST from pyprojen.component import Component +from pyprojen.constructs import Construct from pyprojen.file import FileBase -from pyprojen.util import normalize_persisted_path -from pyprojen.util.constructs import ( - find_closest_project, - is_project, - tag_as_project, -) -from pyprojen.constructs import Construct, Node from pyprojen.ignore_file import IgnoreFile -from pyprojen.common import FILE_MANIFEST from pyprojen.json_file import JsonFile -from pyprojen.cleanup import cleanup, FILE_MANIFEST from pyprojen.object_file import ObjectFile +from pyprojen.util.constructs import tag_as_project # from pyprojen.gitattributes import GitAttributesFile # from pyprojen.tasks import Tasks @@ -32,7 +27,8 @@ DEFAULT_OUTDIR = "." -PROJECT_SYMBOL = 'pyprojen.Project' +PROJECT_SYMBOL = "pyprojen.Project" + class Project(Construct, ABC): """ @@ -77,7 +73,7 @@ def __init__( ".gitignore", filter_comment_lines=git_ignore_filter_comment_lines, filter_empty_lines=git_ignore_filter_empty_lines, - ignore_patterns=git_ignore_patterns + ignore_patterns=git_ignore_patterns, ) @staticmethod @@ -101,7 +97,7 @@ def of(construct: Any) -> "Project": """ if Project.is_project(construct): return construct - if hasattr(construct, 'project'): + if hasattr(construct, "project"): return construct.project raise ValueError(f"{construct.__class__.__name__} is not associated with a Project") @@ -182,7 +178,6 @@ def annotate_generated(self, glob: str): :param glob: The glob pattern to match """ # Implement this method in derived classes - pass def synth(self): """ @@ -218,13 +213,11 @@ def pre_synthesize(self): """ Called before all components are synthesized. """ - pass def post_synthesize(self): """ Called after all components are synthesized. """ - pass @staticmethod def _determine_outdir(parent: Optional["Project"], outdir_option: Optional[str]) -> str: diff --git a/src/pyprojen/textfile.py b/src/pyprojen/textfile.py index c7d83bc..7de1be2 100644 --- a/src/pyprojen/textfile.py +++ b/src/pyprojen/textfile.py @@ -1,7 +1,14 @@ -from typing import List, Optional +from typing import ( + List, + Optional, +) -from pyprojen.file import FileBase, IResolver from pyprojen.constructs import Construct +from pyprojen.file import ( + FileBase, + IResolver, +) + class TextFile(FileBase): """ @@ -17,7 +24,7 @@ def __init__( edit_gitignore: bool = True, readonly: Optional[bool] = None, executable: bool = False, - marker: Optional[bool] = None + marker: Optional[bool] = None, ): """ Initialize a TextFile. @@ -38,14 +45,14 @@ def __init__( edit_gitignore=edit_gitignore, readonly=readonly, executable=executable, - marker=marker + marker=marker, ) self._lines: List[str] = lines or [] def add_line(self, line: str): """ Adds a line to the text file. - + :param line: the line to add (can use tokens) """ self._lines.append(line) @@ -57,4 +64,4 @@ def synthesize_content(self, resolver: IResolver) -> Optional[str]: :param resolver: The resolver to use :return: The synthesized content as a string, or None """ - return "\n".join(self._lines) \ No newline at end of file + return "\n".join(self._lines) diff --git a/src/pyprojen/toml_file.py b/src/pyprojen/toml_file.py index 8e37cc0..aadaa21 100644 --- a/src/pyprojen/toml_file.py +++ b/src/pyprojen/toml_file.py @@ -1,11 +1,16 @@ # Copyright (c) HashiCorp, Inc. # SPDX-License-Identifier: MPL-2.0 -from typing import Any, Optional +from typing import ( + Any, + Optional, +) + import tomlkit from pyprojen.object_file import ObjectFile + class TomlFile(ObjectFile): def __init__( self, @@ -25,10 +30,10 @@ def synthesize_content(self, resolver: Any) -> Optional[str]: return None toml_content = self.serialize(tomlkit.loads(content)) - + if self.marker: return f"# {self.marker}\n\n{toml_content}" return toml_content def serialize(self, obj: Any) -> str: - return tomlkit.dumps(obj) \ No newline at end of file + return tomlkit.dumps(obj) diff --git a/src/pyprojen/util/__init__.py b/src/pyprojen/util/__init__.py index 83aad0f..68febe5 100644 --- a/src/pyprojen/util/__init__.py +++ b/src/pyprojen/util/__init__.py @@ -43,34 +43,34 @@ from .tasks import make_cross_platform # Add these new imports +from .util import deep_merge # Add this line from .util import ( + any_selected, + assert_executable_permissions, + decamelize, + decamelize_keys_recursively, + dedup_array, exec, exec_capture, exec_or_undefined, - get_file_permissions, - write_file, - decamelize_keys_recursively, - is_truthy, - is_object, - deep_merge, # Add this line - dedup_array, - sorted_dict_or_list, + find_up, format_as_python_module, + get_file_permissions, get_git_version, + get_node_major_version, + is_executable, + is_object, + is_root, + is_truthy, + is_writable, kebab_case_keys, + multiple_selected, + normalize_persisted_path, snake_case_keys, + sorted_dict_or_list, try_read_file, try_read_file_sync, - is_writable, - assert_executable_permissions, - is_executable, - decamelize, - get_node_major_version, - any_selected, - multiple_selected, - is_root, - find_up, - normalize_persisted_path, + write_file, ) # Export all imported names diff --git a/src/pyprojen/util/constructs.py b/src/pyprojen/util/constructs.py index 8281da9..aef0e19 100644 --- a/src/pyprojen/util/constructs.py +++ b/src/pyprojen/util/constructs.py @@ -1,54 +1,71 @@ -from typing import Any, Callable, TypeVar, Optional, TYPE_CHECKING +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Optional, + TypeVar, +) if TYPE_CHECKING: from pyprojen.project import Project -T = TypeVar('T') +T = TypeVar("T") + +PROJECT_SYMBOL = "pyprojen.Project" +COMPONENT_SYMBOL = "pyprojen.Component" -PROJECT_SYMBOL = 'pyprojen.Project' -COMPONENT_SYMBOL = 'pyprojen.Component' def try_find_closest(predicate: Callable[[Any], bool]) -> Callable[[Optional[Any]], Optional[T]]: def finder(construct: Optional[Any] = None) -> Optional[T]: if construct is None: return None scopes = [] - if node := getattr(construct, 'node', None): + if node := getattr(construct, "node", None): scopes = node.scopes # scopes = getattr(construct, 'node', {}).get('scopes', []) for scope in reversed(scopes): if predicate(scope): return scope return None + return finder -def find_closest_project(construct: Any) -> 'Project': + +def find_closest_project(construct: Any) -> "Project": from pyprojen.project import Project # Avoid circular import - + if is_component(construct): return construct.project project = try_find_closest(Project.is_project)(construct) if not project: - if node := getattr(construct, 'node', None): + if node := getattr(construct, "node", None): path = node.path if node else "" - raise ValueError(f"{construct.__class__.__name__} at '{path}' " - f"must be created in the scope of a Project, but no Project was found") + raise ValueError( + f"{construct.__class__.__name__} at '{path}' " + f"must be created in the scope of a Project, but no Project was found" + ) return project + def is_project(x: Any) -> bool: from pyprojen.project import Project # Avoid circular import + return Project.is_project(x) + def is_component(x: Any) -> bool: return hasattr(x, COMPONENT_SYMBOL) + def tag_as(scope: Any, tag: str) -> None: setattr(scope, tag, True) + def tag_as_project(scope: Any) -> None: tag_as(scope, PROJECT_SYMBOL) + def tag_as_component(scope: Any) -> None: - tag_as(scope, COMPONENT_SYMBOL) \ No newline at end of file + tag_as(scope, COMPONENT_SYMBOL) diff --git a/src/pyprojen/util/name.py b/src/pyprojen/util/name.py index 3fdc599..179a9aa 100644 --- a/src/pyprojen/util/name.py +++ b/src/pyprojen/util/name.py @@ -1,7 +1,14 @@ -def workflow_name_for_project(base: str, project: 'Project') -> str: +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from pyprojen.project import Project + + +def workflow_name_for_project(base: str, project: "Project") -> str: if project.parent: return f"{base}_{file_safe_name(project.name)}" return base + def file_safe_name(name: str) -> str: - return name.replace("@", "").replace("/", "-") \ No newline at end of file + return name.replace("@", "").replace("/", "-") diff --git a/src/pyprojen/util/object.py b/src/pyprojen/util/object.py index 2414d32..903c706 100644 --- a/src/pyprojen/util/object.py +++ b/src/pyprojen/util/object.py @@ -1,4 +1,8 @@ -from typing import Dict, Any +from typing import ( + Any, + Dict, +) + def remove_null_or_undefined_properties(obj: Dict[str, Any]) -> Dict[str, Any]: """ @@ -7,4 +11,4 @@ def remove_null_or_undefined_properties(obj: Dict[str, Any]) -> Dict[str, Any]: :param obj: The input dictionary :return: A new dictionary with null or undefined properties removed """ - return {k: v for k, v in obj.items() if v is not None} \ No newline at end of file + return {k: v for k, v in obj.items() if v is not None} diff --git a/src/pyprojen/util/path.py b/src/pyprojen/util/path.py index 69ca627..ec8e949 100644 --- a/src/pyprojen/util/path.py +++ b/src/pyprojen/util/path.py @@ -5,10 +5,10 @@ def ensure_relative_path_starts_with_dot(path: str) -> str: :param path: The input path :return: The path with a leading dot if it's relative """ - if path.startswith('.'): + if path.startswith("."): return path - - if path.startswith('/'): + + if path.startswith("/"): raise ValueError(f"Path {path} must be relative") - - return f"./{path}" \ No newline at end of file + + return f"./{path}" diff --git a/src/pyprojen/util/semver.py b/src/pyprojen/util/semver.py index 128a53f..737728d 100644 --- a/src/pyprojen/util/semver.py +++ b/src/pyprojen/util/semver.py @@ -1,6 +1,7 @@ -from enum import Enum import re -from typing import List, Optional +from enum import Enum +from typing import Optional + class TargetName(Enum): """Enum for target names.""" @@ -11,6 +12,7 @@ class TargetName(Enum): GO = 4 JAVASCRIPT = 5 + def to_maven_version_range(semver_range: str, suffix: Optional[str] = None) -> str: """ Convert a semver range to a Maven version range. @@ -21,6 +23,7 @@ def to_maven_version_range(semver_range: str, suffix: Optional[str] = None) -> s """ return to_bracket_notation(semver_range, suffix, semver=False, target=TargetName.JAVA) + def to_nuget_version_range(semver_range: str) -> str: """ Convert a semver range to a NuGet version range. @@ -30,6 +33,7 @@ def to_nuget_version_range(semver_range: str) -> str: """ return to_bracket_notation(semver_range, None, semver=False, target=TargetName.DOTNET) + def to_python_version_range(semver_range: str) -> str: """ Convert a semver range to a Python version range. @@ -39,7 +43,8 @@ def to_python_version_range(semver_range: str) -> str: """ # Implementation of Python version range conversion # This is a simplified version and may need to be expanded - return semver_range.replace('^', '>=').replace('~', '>=') + return semver_range.replace("^", ">=").replace("~", ">=") + def to_release_version(assembly_version: str, target: TargetName) -> str: """ @@ -50,28 +55,27 @@ def to_release_version(assembly_version: str, target: TargetName) -> str: :return: The release version """ version = parse_version(assembly_version) - if not version or not version['prerelease']: + if not version or not version["prerelease"]: return assembly_version if target == TargetName.PYTHON: base_version = f"{version['major']}.{version['minor']}.{version['patch']}" - release_labels = { - 'alpha': 'a', 'beta': 'b', 'rc': 'rc', - 'post': 'post', 'dev': 'dev', 'pre': 'pre' - } - + release_labels = {"alpha": "a", "beta": "b", "rc": "rc", "post": "post", "dev": "dev", "pre": "pre"} + # Simplified prerelease handling for Python - prerelease = '.'.join(version['prerelease']) + prerelease = ".".join(version["prerelease"]) for label, py_label in release_labels.items(): prerelease = prerelease.replace(label, py_label) - + return f"{base_version}.{prerelease}" - + # For other targets, return the original version return assembly_version -def to_bracket_notation(semver_range: str, suffix: Optional[str] = None, - semver: bool = True, target: TargetName = TargetName.JAVASCRIPT) -> str: + +def to_bracket_notation( + semver_range: str, suffix: Optional[str] = None, semver: bool = True, target: TargetName = TargetName.JAVASCRIPT +) -> str: """ Convert a semver range to bracket notation. @@ -82,23 +86,24 @@ def to_bracket_notation(semver_range: str, suffix: Optional[str] = None, :return: The bracket notation """ # This is a simplified implementation and may need to be expanded - if semver_range == '*': - return '[0.0.0,)' - + if semver_range == "*": + return "[0.0.0,)" + # Handle basic ranges - if semver_range.startswith('^'): + if semver_range.startswith("^"): version = semver_range[1:] parsed = parse_version(version) if parsed: - if parsed['major'] == 0: + if parsed["major"] == 0: return f"[{version},{parsed['major']}.{parsed['minor'] + 1}.0)" else: return f"[{version},{parsed['major'] + 1}.0.0)" - + # For more complex ranges, you may need to implement additional logic - + return semver_range + def parse_version(version: str) -> Optional[dict]: """ Parse a version string into its components. @@ -106,13 +111,13 @@ def parse_version(version: str) -> Optional[dict]: :param version: The version string :return: A dictionary of version components, or None if parsing fails """ - match = re.match(r'^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-.]+))?(?:\+([0-9A-Za-z-]+))?$', version) + match = re.match(r"^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-.]+))?(?:\+([0-9A-Za-z-]+))?$", version) if match: return { - 'major': int(match.group(1)), - 'minor': int(match.group(2)), - 'patch': int(match.group(3)), - 'prerelease': match.group(4).split('.') if match.group(4) else [], - 'build': match.group(5).split('.') if match.group(5) else [] + "major": int(match.group(1)), + "minor": int(match.group(2)), + "patch": int(match.group(3)), + "prerelease": match.group(4).split(".") if match.group(4) else [], + "build": match.group(5).split(".") if match.group(5) else [], } - return None \ No newline at end of file + return None diff --git a/src/pyprojen/util/synth.py b/src/pyprojen/util/synth.py index 3fd17b9..97cd138 100644 --- a/src/pyprojen/util/synth.py +++ b/src/pyprojen/util/synth.py @@ -1,12 +1,17 @@ -import os +import glob import json +import os import tempfile -from typing import Dict, Any, TYPE_CHECKING -import glob +from typing import ( + TYPE_CHECKING, + Any, + Dict, +) if TYPE_CHECKING: from pyprojen.project import Project + class SnapshotOptions: """Options for creating a snapshot.""" @@ -18,7 +23,8 @@ def __init__(self, parse_json: bool = True): """ self.parse_json = parse_json -def synth_snapshot(project: 'Project', options: SnapshotOptions = SnapshotOptions()) -> Dict[str, Any]: + +def synth_snapshot(project: "Project", options: SnapshotOptions = SnapshotOptions()) -> Dict[str, Any]: """ Create a snapshot of the synthesized project. @@ -26,29 +32,37 @@ def synth_snapshot(project: 'Project', options: SnapshotOptions = SnapshotOption :param options: Options for creating the snapshot :return: A dictionary representing the snapshot """ - if not project.outdir.startswith(tempfile.gettempdir()) and 'project-temp-dir' not in project.outdir: - raise ValueError("Trying to capture a snapshot of a project outside of tmpdir, which implies this test might corrupt an existing project") + if not project.outdir.startswith(tempfile.gettempdir()) and "project-temp-dir" not in project.outdir: + raise ValueError( + "Trying to capture a snapshot of a project outside of tmpdir, which implies this test might corrupt an existing project" + ) - if hasattr(project, '_synthed'): + if hasattr(project, "_synthed"): raise ValueError("duplicate synth()") project._synthed = True - old_env = os.environ.get('PROJEN_DISABLE_POST') + old_env = os.environ.get("PROJEN_DISABLE_POST") try: - os.environ['PROJEN_DISABLE_POST'] = 'true' + os.environ["PROJEN_DISABLE_POST"] = "true" project.synth() - ignore_exts = ['png', 'ico'] - return directory_snapshot(project.outdir, { - **options.__dict__, - 'exclude_globs': [f'**/*.{ext}' for ext in ignore_exts], - 'support_json_comments': any(getattr(file, 'supports_comments', False) for file in project.files if isinstance(file, JsonFile)) - }) + ignore_exts = ["png", "ico"] + return directory_snapshot( + project.outdir, + { + **options.__dict__, + "exclude_globs": [f"**/*.{ext}" for ext in ignore_exts], + "support_json_comments": any( + getattr(file, "supports_comments", False) for file in project.files if isinstance(file, JsonFile) + ), + }, + ) finally: if old_env is None: - del os.environ['PROJEN_DISABLE_POST'] + del os.environ["PROJEN_DISABLE_POST"] else: - os.environ['PROJEN_DISABLE_POST'] = old_env + os.environ["PROJEN_DISABLE_POST"] = old_env + def directory_snapshot(root: str, options: Dict[str, Any] = {}) -> Dict[str, Any]: """ @@ -59,24 +73,28 @@ def directory_snapshot(root: str, options: Dict[str, Any] = {}) -> Dict[str, Any :return: A dictionary representing the snapshot """ output = {} - files = glob.glob('**', recursive=True, root_dir=root) - files = [f for f in files if not f.startswith('.git/') and not any(f.endswith(ext) for ext in options.get('exclude_globs', []))] + files = glob.glob("**", recursive=True, root_dir=root) + files = [ + f + for f in files + if not f.startswith(".git/") and not any(f.endswith(ext) for ext in options.get("exclude_globs", [])) + ] - parse_json = options.get('parse_json', True) + parse_json = options.get("parse_json", True) for file in files: file_path = os.path.join(root, file) if os.path.isfile(file_path): - if options.get('only_file_names', False): + if options.get("only_file_names", False): output[file] = True else: - with open(file_path, 'r') as f: + with open(file_path, "r") as f: content = f.read() - if parse_json and file.lower().endswith(('.json', '.json5', '.jsonc')): + if parse_json and file.lower().endswith((".json", ".json5", ".jsonc")): try: content = json.loads(content) except json.JSONDecodeError: pass # Keep content as string if it's not valid JSON output[file] = content - return output \ No newline at end of file + return output diff --git a/src/pyprojen/util/tasks.py b/src/pyprojen/util/tasks.py index f1bf0f6..c12d350 100644 --- a/src/pyprojen/util/tasks.py +++ b/src/pyprojen/util/tasks.py @@ -1,11 +1,13 @@ import platform + def make_cross_platform(command: str) -> str: - if platform.system() != 'Windows': + if platform.system() != "Windows": return command - return ' && '.join( - f"shx {subcommand.strip()}" if subcommand.strip().split()[0] in ['cat', 'cp', 'mkdir', 'mv', 'rm'] + return " && ".join( + f"shx {subcommand.strip()}" + if subcommand.strip().split()[0] in ["cat", "cp", "mkdir", "mv", "rm"] else subcommand.strip() - for subcommand in command.split('&&') - ) \ No newline at end of file + for subcommand in command.split("&&") + ) diff --git a/src/pyprojen/util/util.py b/src/pyprojen/util/util.py index 8a8dd74..3c3bc7e 100644 --- a/src/pyprojen/util/util.py +++ b/src/pyprojen/util/util.py @@ -1,16 +1,12 @@ -import json import os import platform import re import subprocess -import tempfile from typing import ( Any, - Callable, Dict, List, Optional, - Union, ) MAX_BUFFER = 10 * 1024 * 1024 @@ -122,6 +118,7 @@ def deep_merge(objects: List[Optional[Dict[str, Any]]], destructive: bool = Fals :param destructive: Whether to delete keys with None values :return: Merged dictionary """ + def merge_one(target: Dict[str, Any], source: Dict[str, Any]) -> None: for key, value in source.items(): if isinstance(value, dict): diff --git a/src/pyprojen/yaml_file.py b/src/pyprojen/yaml_file.py index 73f957b..23e8655 100644 --- a/src/pyprojen/yaml_file.py +++ b/src/pyprojen/yaml_file.py @@ -1,10 +1,16 @@ # Copyright (c) HashiCorp, Inc. # SPDX-License-Identifier: MPL-2.0 -from typing import Any, Optional -from pyprojen.object_file import ObjectFile +from typing import ( + Any, + Optional, +) + import yaml +from pyprojen.object_file import ObjectFile + + class YamlFile(ObjectFile): """ Represents a YAML file. @@ -30,10 +36,10 @@ def synthesize_content(self, resolver: Any) -> Optional[str]: return None yaml_content = self.serialize(yaml.safe_load(content)) - + if self.marker: return f"# {self.marker}\n\n{yaml_content}" return yaml_content def serialize(self, obj: Any) -> str: - return yaml.dump(obj, default_flow_style=False, width=self.line_width) \ No newline at end of file + return yaml.dump(obj, default_flow_style=False, width=self.line_width) diff --git a/tests/unit_tests/test__example.py b/tests/unit_tests/test__example.py index 7fa1dc4..9fe4aab 100644 --- a/tests/unit_tests/test__example.py +++ b/tests/unit_tests/test__example.py @@ -1,5 +1,6 @@ """Example test file in place so that a freshly created project can immediately pass tests.""" + def test__example(): """An example test to demonstrate how to write unit tests.""" - assert True \ No newline at end of file + assert True