Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions synth_a_py/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
__version__ = "1.6.0"

from .base import Dir, File, Project
from .base import Dir, File, Project, auto_synth
from .file import EmptyFile, SimpleFile
from .gitignore import GitIgnore
from .ini import IniFile
from .license import License
from .license import License, LicenseBase
from .poetry import PoetryModule
from .tokens import Managed
from .toml import TomlFile
from .yaml import YamlFile

Expand All @@ -15,8 +17,13 @@
"GitIgnore",
"IniFile",
"License",
"LicenseBase",
"Managed",
"Poetry",
"Project",
"PoetryModule",
"SimpleFile",
"TomlFile",
"YamlFile",
"auto_synth",
]
164 changes: 84 additions & 80 deletions synth_a_py/base.py
Original file line number Diff line number Diff line change
@@ -1,123 +1,100 @@
from abc import abstractmethod
from abc import ABC, abstractmethod
from contextlib import contextmanager
from contextvars import ContextVar, Token
from pathlib import Path
from types import TracebackType
from typing import TYPE_CHECKING, Callable, Dict, Iterator, Optional, Tuple, Type, Union
from typing import Callable, Dict, Iterator, Optional, Tuple, Type, Union

from returns.functions import compose

from .utils import init_mix_ins

__all__ = [
"File",
"Project",
"Dir",
"File",
"auto_synth",
]


__container_context: "ContextVar[Optional[Container]]" = ContextVar(
"__container_context", default=None
)


def _context_get() -> "Container":
container = __container_context.get()
assert container is not None
return container


def _context_set_root(
value: "Container",
) -> "Token[Optional[Container]]":
container = __container_context.get()
assert container is None
return __container_context.set(value)


def _context_set(value: "Container") -> "Token[Optional[Container]]":
container = __container_context.get()
assert container is not None
return __container_context.set(value)


def _context_reset(token: "Token[Optional[Container]]") -> None:
container = __container_context.get()
assert container is not None
__container_context.reset(token)


PathResolver = Callable[[Path], Path]


class _FileContainerMixIn:
class Container(ABC):
def __init__(self) -> None:
self.__store: Dict[str, Union[File, Dir]] = dict()
self._store: "Dict[str, Union[File, Dir]]" = dict()
self._context_token: "Optional[Token[Optional[Container]]]" = None

def add(self, item: Union["File", "Dir"]) -> None:
assert item.name not in self.__store
self.__store[item.name] = item
assert item.name not in self._store
self._store[item.name] = item

def walk(self) -> Iterator[Tuple[PathResolver, "File"]]:
item: Union[File, Dir]
for item in self.__store.values():
for item in self._store.values():

def path_resolver(path: Path) -> Path:
return path / item.name

if isinstance(item, File):
yield path_resolver, item
else:
if isinstance(item, Container):
for subpath_resolver, subitem in item.walk():
yield compose(path_resolver, subpath_resolver), subitem
else:
yield path_resolver, item

def subpaths(self) -> Iterator[str]:
return (str(path_resolver(Path("."))) for path_resolver, _ in self.walk())


if TYPE_CHECKING:
_ContextToken = Token[Optional[_FileContainerMixIn]]
else:
_ContextToken = Token

__context: ContextVar[Optional[_FileContainerMixIn]] = ContextVar(
"__context", default=None
)


def _context_get() -> _FileContainerMixIn:
project = __context.get()
assert project is not None
return project


def _context_set_root(value: _FileContainerMixIn) -> _ContextToken:
project = __context.get()
assert project is None
return __context.set(value)


def _context_set(value: _FileContainerMixIn) -> _ContextToken:
project = __context.get()
assert project is not None
return __context.set(value)


def _context_reset(token: _ContextToken) -> None:
project = __context.get()
assert project is not None
__context.reset(token)


class _ChildMixIn:
def __init__(self) -> None:
self.parent = _context_get()
assert isinstance(self, File) or isinstance(self, Dir)
self.parent.add(self)


class File(_ChildMixIn):
def __init__(self, name: str) -> None:
self.name = name
init_mix_ins(self, File)

@abstractmethod
def synth_content(self) -> str:
def __enter__(self) -> "Container":
...


class _ContextMixIn(_FileContainerMixIn):
def __init__(self) -> None:
self.__context_token: Optional[_ContextToken] = None
init_mix_ins(self, _ContextMixIn)

def __enter__(self) -> None:
assert self.__context_token is None
if isinstance(self, Project):
self.__context_token = _context_set_root(self)
else:
self.__context_token = _context_set(self)

def __exit__(
self,
exc_type: Optional[Type[BaseException]],
exc_value: Optional[BaseException],
traceback: Optional[TracebackType],
) -> None:
assert self.__context_token is not None
_context_reset(self.__context_token)
self.__context_token = None
assert self._context_token is not None
_context_reset(self._context_token)
self._context_token = None


class Project(Container):
def __enter__(self) -> "Project":
assert self._context_token is None
self._context_token = _context_set_root(self)
return self

class Project(_ContextMixIn):
def synth(self, root: Optional[Path] = None) -> None:
if root is None:
root = Path.cwd()
Expand All @@ -138,7 +115,34 @@ def synth(self, root: Optional[Path] = None) -> None:
path.chmod(0o444)


class Dir(_ContextMixIn, _ChildMixIn):
class Dir(Container):
def __init__(self, name: str) -> None:
super().__init__()
self.name = name
init_mix_ins(self, Dir)
_context_get().add(self)

def __enter__(self) -> "Dir":
assert self._context_token is None
self._context_token = _context_set(self)
return self


class File:
def __init__(self, name: str) -> None:
self.name = name
self.parent = _context_get()
self.parent.add(self)

@abstractmethod
def synth_content(self) -> str:
...


@contextmanager
def auto_synth(root: Optional[Path] = None) -> Iterator[Project]:
spec = Project()

with spec:
yield spec

spec.synth(root)
12 changes: 8 additions & 4 deletions synth_a_py/license.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
from abc import ABC
from textwrap import dedent
from typing import Type

from typing_extensions import Literal

from .base import File

__all__ = ["License"]
__all__ = [
"LicenseBase",
"License",
]


class _License(File):
class LicenseBase(File, ABC):
def __init__(self, copyright_period: str, copyright_holders: str):
super().__init__("LICENSE")
self.copyright_period = copyright_period
self.copyright_holders = copyright_holders


class _ApacheLicense(_License):
class _ApacheLicense(LicenseBase):
def __init__(
self, version: Literal["2.0"], copyright_period: str, copyright_holders: str
) -> None:
Expand Down Expand Up @@ -227,7 +231,7 @@ def synth_content(self) -> str:
""" # noqa: E501


class _MITLicense(_License):
class _MITLicense(LicenseBase):
def synth_content(self) -> str:
return dedent(
f"""\
Expand Down
108 changes: 108 additions & 0 deletions synth_a_py/poetry/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
from typing import Any, Dict, List, Optional, cast

from synth_a_py.base import Dir
from synth_a_py.poetry.versions import (
DependencyDict,
ManagableDependencyDict,
ManagableVersion,
Managed,
Version,
VersionSpec,
)
from synth_a_py.toml import TomlFile

__all__ = [
"PoetryModule",
]


def resolve_dependencies(
managable_dependencies: ManagableDependencyDict,
dependency_management: DependencyDict,
) -> DependencyDict:
if not dependency_management:
return cast(DependencyDict, managable_dependencies)

def get_managed_version(dep: str) -> Version:
try:
return dependency_management[dep]
except Exception as e:
raise Exception(f"Dependency {dep} missing from dependency_management", e)

def resolve_dependency(dep: str, version: ManagableVersion) -> Version:
if isinstance(version, str):
return version
elif version == Managed:
return get_managed_version(dep)
elif isinstance(version, dict) and version["version"] == Managed:
managed_version = get_managed_version(dep)
resolved_version: VersionSpec = {
"version": managed_version
if isinstance(managed_version, str)
else managed_version["version"]
}

if "extras" in version:
resolved_version["extras"] = version["extras"]

return resolved_version
else:
return cast(VersionSpec, version)

return {
dep: resolve_dependency(dep, version)
for dep, version in managable_dependencies.items()
}


class PoetryModule:
def __init__(
self,
*,
name: str,
description: str,
version: str,
authors: Optional[List[str]] = None,
license: Optional[str] = None,
dependencies: Optional[ManagableDependencyDict] = None,
dev_dependencies: Optional[ManagableDependencyDict] = None,
dependency_management: Optional[DependencyDict] = None,
):
self.dir = Dir(name)
with self.dir:
pyproject_tool_poetry: Dict[str, Any] = {
"name": name,
"description": description,
"version": version,
"authors": authors or [],
}

if license:
pyproject_tool_poetry["license"] = license

dependency_management = dependency_management or {}

if dependencies:
pyproject_tool_poetry["dependencies"] = resolve_dependencies(
dependencies,
dependency_management,
)

if dev_dependencies:
pyproject_tool_poetry["dev-dependencies"] = resolve_dependencies(
dev_dependencies,
dependency_management,
)

self.pyproject = TomlFile(
"pyproject.toml",
{
"build-system": {
"requires": ["poetry-core>=1.0.0"],
"build-backend": "poetry.core.masonry.api",
},
"tool": {
"poetry": pyproject_tool_poetry,
},
},
)
Loading