Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support namespace packages with new command and misc. improvements #2768

Merged
merged 7 commits into from
Apr 12, 2021
Merged
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
31 changes: 25 additions & 6 deletions docs/docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,11 @@ will create a folder as follows:
```text
my-package
├── pyproject.toml
├── README.rst
├── README.md
├── my_package
│ └── __init__.py
└── tests
├── __init__.py
└── test_my_package.py
└── __init__.py
```

If you want to name your project differently than the folder, you can pass
Expand All @@ -57,13 +56,33 @@ That will create a folder structure as follows:
```text
my-package
├── pyproject.toml
├── README.rst
├── README.md
├── src
│ └── my_package
│ └── __init__.py
└── tests
├── __init__.py
└── test_my_package.py
└── __init__.py
```

The `--name` option is smart enough to detect namespace packages and create
the required structure for you.

```bash
poetry new --src --name my.package my-package
```

will create the following structure:

```text
my-package
├── pyproject.toml
├── README.md
├── src
│ └── my
│ └── package
│ └── __init__.py
└── tests
└── __init__.py
```

## init
Expand Down
34 changes: 20 additions & 14 deletions poetry/console/commands/new.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
from cleo.helpers import argument
from cleo.helpers import option

from poetry.utils.helpers import module_name

from .command import Command


Expand All @@ -17,12 +15,17 @@ class NewCommand(Command):
options = [
option("name", None, "Set the resulting package name.", flag=False),
option("src", None, "Use the src layout for the project."),
option(
"readme",
None,
"Specify the readme file format. One of md (default) or rst",
flag=False,
),
]

def handle(self) -> None:
from pathlib import Path

from poetry.core.semver.helpers import parse_constraint
from poetry.core.vcs.git import GitConfig
from poetry.layouts import layout
from poetry.utils.env import SystemEnv
Expand All @@ -32,7 +35,11 @@ def handle(self) -> None:
else:
layout_ = layout("standard")

path = Path.cwd() / Path(self.argument("path"))
path = Path(self.argument("path"))
if not path.is_absolute():
# we do not use resolve here due to compatibility issues for path.resolve(strict=False)
path = Path.cwd().joinpath(path)

name = self.option("name")
if not name:
name = path.name
Expand All @@ -45,7 +52,7 @@ def handle(self) -> None:
"exists and is not empty".format(path)
)

readme_format = "rst"
readme_format = self.option("readme") or "md"

config = GitConfig()
author = None
Expand All @@ -60,25 +67,24 @@ def handle(self) -> None:
".".join(str(v) for v in current_env.version_info[:2])
)

dev_dependencies = {}
python_constraint = parse_constraint(default_python)
if parse_constraint("<3.5").allows_any(python_constraint):
dev_dependencies["pytest"] = "^4.6"
if parse_constraint(">=3.5").allows_all(python_constraint):
dev_dependencies["pytest"] = "^5.2"

layout_ = layout_(
name,
"0.1.0",
author=author,
readme_format=readme_format,
abn marked this conversation as resolved.
Show resolved Hide resolved
python=default_python,
dev_dependencies=dev_dependencies,
)
layout_.create(path)

path = path.resolve()

try:
path = path.relative_to(Path.cwd())
except ValueError:
pass

self.line(
"Created package <info>{}</> in <fg=blue>{}</>".format(
module_name(name), path.relative_to(Path.cwd())
layout_._package_name, path.as_posix() # noqa
)
)
3 changes: 1 addition & 2 deletions poetry/layouts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@

from .layout import Layout
from .src import SrcLayout
from .standard import StandardLayout


_LAYOUTS = {"src": SrcLayout, "standard": StandardLayout}
_LAYOUTS = {"src": SrcLayout, "standard": Layout}


def layout(name: str) -> Type[Layout]:
Expand Down
123 changes: 77 additions & 46 deletions poetry/layouts/layout.py
Original file line number Diff line number Diff line change
@@ -1,74 +1,73 @@
from pathlib import Path
from typing import TYPE_CHECKING
from typing import Dict
from typing import Optional

from tomlkit import dumps
from tomlkit import inline_table
from tomlkit import loads
from tomlkit import table

from poetry.utils.helpers import canonicalize_name
from poetry.utils.helpers import module_name


if TYPE_CHECKING:
from pathlib import Path
from tomlkit.items import InlineTable

from poetry.core.pyproject.toml import PyProjectTOML

TESTS_DEFAULT = """from {package_name} import __version__


def test_version():
assert __version__ == '{version}'
"""


POETRY_DEFAULT = """\
[tool.poetry]
name = ""
version = ""
description = ""
authors = []

[tool.poetry.dependencies]

[tool.poetry.dev-dependencies]
"""

POETRY_WITH_LICENSE = """\
[tool.poetry]
name = ""
version = ""
description = ""
authors = []
license = ""
readme = ""
packages = []

[tool.poetry.dependencies]

[tool.poetry.dev-dependencies]
"""

BUILD_SYSTEM_MIN_VERSION = "1.0.0"
BUILD_SYSTEM_MIN_VERSION: Optional[str] = None
BUILD_SYSTEM_MAX_VERSION: Optional[str] = None


class Layout:
ACCEPTED_README_FORMATS = {"md", "rst"}

def __init__(
self,
project: str,
version: str = "0.1.0",
description: str = "",
readme_format: str = "md",
author: Optional[str] = None,
license: Optional[str] = None,
license: Optional[str] = None, # noqa
python: str = "*",
dependencies: Optional[Dict[str, str]] = None,
dev_dependencies: Optional[Dict[str, str]] = None,
):
self._project = project
self._package_name = module_name(project)
self._project = canonicalize_name(project).replace(".", "-")
self._package_path_relative = Path(
*(module_name(part) for part in canonicalize_name(project).split("."))
)
self._package_name = ".".join(self._package_path_relative.parts)
self._version = version
self._description = description
self._readme_format = readme_format

self._readme_format = readme_format.lower()
if self._readme_format not in self.ACCEPTED_README_FORMATS:
raise ValueError(
"Invalid readme format '{}', use one of {}.".format(
readme_format, ", ".join(self.ACCEPTED_README_FORMATS)
)
)

self._license = license
self._python = python
self._dependencies = dependencies or {}
Expand All @@ -79,6 +78,30 @@ def __init__(

self._author = author

@property
def basedir(self) -> Path:
return Path()

@property
def package_path(self) -> Path:
return self.basedir / self._package_path_relative

def get_package_include(self) -> Optional["InlineTable"]:
package = inline_table()

include = self._package_path_relative.parts[0]
package.append("include", include)

if self.basedir != Path():
package.append("from", self.basedir.as_posix())
else:
if include == self._project:
# package include and package name are the same,
# packages table is redundant here.
return None

return package

def create(self, path: "Path", with_tests: bool = True) -> None:
path.mkdir(parents=True, exist_ok=True)

Expand All @@ -94,17 +117,26 @@ def generate_poetry_content(
self, original: Optional["PyProjectTOML"] = None
) -> str:
template = POETRY_DEFAULT
if self._license:
template = POETRY_WITH_LICENSE

content = loads(template)

poetry_content = content["tool"]["poetry"]
poetry_content["name"] = self._project
poetry_content["version"] = self._version
poetry_content["description"] = self._description
poetry_content["authors"].append(self._author)

if self._license:
poetry_content["license"] = self._license
else:
poetry_content.remove("license")

poetry_content["readme"] = "README.{}".format(self._readme_format)
packages = self.get_package_include()
if packages:
poetry_content["packages"].append(packages)
else:
poetry_content.remove("packages")

poetry_content["dependencies"]["python"] = self._python

Expand All @@ -116,9 +148,14 @@ def generate_poetry_content(

# Add build system
build_system = table()
build_system_version = ">=" + BUILD_SYSTEM_MIN_VERSION
build_system_version = ""

if BUILD_SYSTEM_MIN_VERSION is not None:
build_system_version = ">=" + BUILD_SYSTEM_MIN_VERSION
if BUILD_SYSTEM_MAX_VERSION is not None:
build_system_version += ",<" + BUILD_SYSTEM_MAX_VERSION
if build_system_version:
build_system_version += ","
build_system_version += "<" + BUILD_SYSTEM_MAX_VERSION
abn marked this conversation as resolved.
Show resolved Hide resolved

build_system.add("requires", ["poetry-core" + build_system_version])
build_system.add("build-backend", "poetry.core.masonry.api")
Expand All @@ -133,30 +170,24 @@ def generate_poetry_content(
return content

def _create_default(self, path: "Path", src: bool = True) -> None:
raise NotImplementedError()
package_path = path / self.package_path
package_path.mkdir(parents=True)

def _create_readme(self, path: "Path") -> None:
if self._readme_format == "rst":
readme_file = path / "README.rst"
else:
readme_file = path / "README.md"
package_init = package_path / "__init__.py"
package_init.touch()

def _create_readme(self, path: "Path") -> "Path":
readme_file = path.joinpath("README.{}".format(self._readme_format))
readme_file.touch()
return readme_file

def _create_tests(self, path: "Path") -> None:
@staticmethod
def _create_tests(path: "Path") -> None:
tests = path / "tests"
tests_init = tests / "__init__.py"
tests_default = tests / f"test_{self._package_name}.py"

tests.mkdir()
tests_init.touch(exist_ok=False)

with tests_default.open("w", encoding="utf-8") as f:
f.write(
TESTS_DEFAULT.format(
package_name=self._package_name, version=self._version
)
)
tests_init = tests / "__init__.py"
tests_init.touch(exist_ok=False)

def _write_poetry(self, path: "Path") -> None:
content = self.generate_poetry_content()
Expand Down
21 changes: 4 additions & 17 deletions poetry/layouts/src.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,9 @@
from typing import TYPE_CHECKING
from pathlib import Path

from .layout import Layout


if TYPE_CHECKING:
from pathlib import Path

DEFAULT = """__version__ = '{version}'
"""


class SrcLayout(Layout):
def _create_default(self, path: "Path") -> None:
package_path = path / "src" / self._package_name

package_init = package_path / "__init__.py"

package_path.mkdir(parents=True)

with package_init.open("w", encoding="utf-8") as f:
f.write(DEFAULT.format(version=self._version))
@property
def basedir(self) -> "Path":
return Path("src")
Loading