Commit bf2d515a by Sébastien Eustace Committed by GitHub

Improve git dependencies support (#1549)

* Improve the InstalledRepository class

* Improve support for git dependencies
parent b2189691
...@@ -2,7 +2,8 @@ ...@@ -2,7 +2,8 @@
# Packages # Packages
*.egg *.egg
*.egg-info /*.egg-info
/tests/fixtures/**/*.egg-info
/dist/* /dist/*
build build
_build _build
......
...@@ -20,5 +20,7 @@ repos: ...@@ -20,5 +20,7 @@ repos:
rev: v2.3.0 rev: v2.3.0
hooks: hooks:
- id: trailing-whitespace - id: trailing-whitespace
exclude: ^tests/.*/fixtures/.*
- id: end-of-file-fixer - id: end-of-file-fixer
exclude: ^tests/.*/fixtures/.*
- id: debug-statements - id: debug-statements
...@@ -208,8 +208,8 @@ If you need to checkout a specific branch, tag or revision, ...@@ -208,8 +208,8 @@ If you need to checkout a specific branch, tag or revision,
you can specify it when using `add`: you can specify it when using `add`:
```bash ```bash
poetry add git+https://github.com/sdispater/pendulum.git@develop poetry add git+https://github.com/sdispater/pendulum.git#develop
poetry add git+https://github.com/sdispater/pendulum.git@2.0.5 poetry add git+https://github.com/sdispater/pendulum.git#2.0.5
``` ```
or make them point to a local directory or file: or make them point to a local directory or file:
......
...@@ -1255,7 +1255,7 @@ docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] ...@@ -1255,7 +1255,7 @@ docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
testing = ["pathlib2", "contextlib2", "unittest2"] testing = ["pathlib2", "contextlib2", "unittest2"]
[metadata] [metadata]
content-hash = "0a019fe9f27e3e3fc226c506b4c9de219d7b9d021e998ce88563f7b9c945aa35" content-hash = "a4df5ba98a54bb2d869cba83dd16d970a5da429185a72ca1dd837d5e5c7a4335"
python-versions = "~2.7 || ^3.4" python-versions = "~2.7 || ^3.4"
[metadata.files] [metadata.files]
......
...@@ -147,8 +147,8 @@ The <c1>init</c1> command creates a basic <comment>pyproject.toml</> file in the ...@@ -147,8 +147,8 @@ The <c1>init</c1> command creates a basic <comment>pyproject.toml</> file in the
"You can specify a package in the following forms:\n" "You can specify a package in the following forms:\n"
" - A single name (<b>requests</b>)\n" " - A single name (<b>requests</b>)\n"
" - A name and a constraint (<b>requests ^2.23.0</b>)\n" " - A name and a constraint (<b>requests ^2.23.0</b>)\n"
" - A git url (<b>https://github.com/sdispater/poetry.git</b>)\n" " - A git url (<b>git+https://github.com/sdispater/poetry.git</b>)\n"
" - A git url with a revision (<b>https://github.com/sdispater/poetry.git@develop</b>)\n" " - A git url with a revision (<b>git+https://github.com/sdispater/poetry.git#develop</b>)\n"
" - A file path (<b>../my-package/my-package.whl</b>)\n" " - A file path (<b>../my-package/my-package.whl</b>)\n"
" - A directory (<b>../my-package/</b>)\n" " - A directory (<b>../my-package/</b>)\n"
" - An url (<b>https://example.com/packages/my-package-0.1.0.tar.gz</b>)\n" " - An url (<b>https://example.com/packages/my-package-0.1.0.tar.gz</b>)\n"
...@@ -365,22 +365,21 @@ The <c1>init</c1> command creates a basic <comment>pyproject.toml</> file in the ...@@ -365,22 +365,21 @@ The <c1>init</c1> command creates a basic <comment>pyproject.toml</> file in the
if url_parsed.scheme and url_parsed.netloc: if url_parsed.scheme and url_parsed.netloc:
# Url # Url
if url_parsed.scheme in ["git+https", "git+ssh"]: if url_parsed.scheme in ["git+https", "git+ssh"]:
url = requirement.lstrip("git+") from poetry.vcs.git import Git
rev = None from poetry.vcs.git import ParsedUrl
if "@" in url:
url, rev = url.split("@")
pair = OrderedDict( parsed = ParsedUrl.parse(requirement)
[("name", url.split("/")[-1].rstrip(".git")), ("git", url)] url = Git.normalize_url(requirement)
)
if rev: pair = OrderedDict([("name", parsed.name), ("git", url.url)])
pair["rev"] = rev if parsed.rev:
pair["rev"] = url.revision
if extras: if extras:
pair["extras"] = extras pair["extras"] = extras
package = Provider.get_package_from_vcs( package = Provider.get_package_from_vcs(
"git", url, reference=pair.get("rev") "git", url.url, reference=pair.get("rev")
) )
pair["name"] = package.name pair["name"] = package.name
result.append(pair) result.append(pair)
......
...@@ -56,12 +56,17 @@ class Solver: ...@@ -56,12 +56,17 @@ class Solver:
installed = True installed = True
if pkg.source_type == "git" and package.source_type == "git": if pkg.source_type == "git" and package.source_type == "git":
from poetry.vcs.git import Git
# Trying to find the currently installed version # Trying to find the currently installed version
pkg_source_url = Git.normalize_url(pkg.source_url)
package_source_url = Git.normalize_url(package.source_url)
for locked in self._locked.packages: for locked in self._locked.packages:
locked_source_url = Git.normalize_url(locked.source_url)
if ( if (
locked.name == pkg.name locked.name == pkg.name
and locked.source_type == pkg.source_type and locked.source_type == pkg.source_type
and locked.source_url == pkg.source_url and locked_source_url == pkg_source_url
and locked.source_reference == pkg.source_reference and locked.source_reference == pkg.source_reference
): ):
pkg = Package(pkg.name, locked.version) pkg = Package(pkg.name, locked.version)
...@@ -70,7 +75,7 @@ class Solver: ...@@ -70,7 +75,7 @@ class Solver:
pkg.source_reference = locked.source_reference pkg.source_reference = locked.source_reference
break break
if pkg.source_url != package.source_url or ( if pkg_source_url != package_source_url or (
pkg.source_reference != package.source_reference pkg.source_reference != package.source_reference
and not pkg.source_reference.startswith( and not pkg.source_reference.startswith(
package.source_reference package.source_reference
...@@ -84,6 +89,8 @@ class Solver: ...@@ -84,6 +89,8 @@ class Solver:
elif package.version != pkg.version: elif package.version != pkg.version:
# Checking version # Checking version
operations.append(Update(pkg, package)) operations.append(Update(pkg, package))
elif package.source_type != pkg.source_type:
operations.append(Update(pkg, package))
else: else:
operations.append(Install(package).skip("Already installed")) operations.append(Install(package).skip("Already installed"))
......
import re from importlib_metadata import distributions
from poetry.packages import Package from poetry.packages import Package
from poetry.utils._compat import Path
from poetry.utils.env import Env from poetry.utils.env import Env
from .repository import Repository from .repository import Repository
...@@ -16,29 +16,45 @@ class InstalledRepository(Repository): ...@@ -16,29 +16,45 @@ class InstalledRepository(Repository):
""" """
repo = cls() repo = cls()
freeze_output = env.run("python", "-m", "pip", "freeze") for distribution in sorted(
for line in freeze_output.split("\n"): distributions(path=env.sys_path), key=lambda d: str(d._path),
if "==" in line: ):
name, version = re.split("={2,3}", line) metadata = distribution.metadata
repo.add_package(Package(name, version, version)) name = metadata["name"]
elif line.startswith("-e "): version = metadata["version"]
line = line[3:].strip() package = Package(name, version, version)
if line.startswith("git+"): package.description = metadata.get("summary", "")
url = line.lstrip("git+")
if "@" in url: repo.add_package(package)
url, rev = url.rsplit("@", 1)
else: path = Path(distribution._path)
rev = "master" is_standard_package = True
try:
name = url.split("/")[-1].rstrip(".git") path.relative_to(env.site_packages)
if "#egg=" in rev: except ValueError:
rev, name = rev.split("#egg=") is_standard_package = False
package = Package(name, "0.0.0") if is_standard_package:
package.source_type = "git" continue
package.source_url = url
package.source_reference = rev src_path = env.path / "src"
repo.add_package(package) # A VCS dependency should have been installed
# in the src directory. If not, it's a path dependency
try:
path.relative_to(src_path)
from poetry.vcs.git import Git
git = Git()
revision = git.rev_parse("HEAD", src_path / package.name).strip()
url = git.remote_url(src_path / package.name)
package.source_type = "git"
package.source_url = url
package.source_reference = revision
except ValueError:
package.source_type = "directory"
package.source_url = str(path.parent)
return repo return repo
...@@ -94,6 +94,13 @@ import sys ...@@ -94,6 +94,13 @@ import sys
print('.'.join([str(s) for s in sys.version_info[:3]])) print('.'.join([str(s) for s in sys.version_info[:3]]))
""" """
GET_SYS_PATH = """\
import json
import sys
print(json.dumps(sys.path))
"""
CREATE_VENV_COMMAND = """\ CREATE_VENV_COMMAND = """\
path = {!r} path = {!r}
...@@ -742,6 +749,10 @@ class Env(object): ...@@ -742,6 +749,10 @@ class Env(object):
/ "site-packages" / "site-packages"
) )
@property
def sys_path(self): # type: () -> List[str]
raise NotImplementedError()
@classmethod @classmethod
def get_base_prefix(cls): # type: () -> Path def get_base_prefix(cls): # type: () -> Path
if hasattr(sys, "real_prefix"): if hasattr(sys, "real_prefix"):
...@@ -865,6 +876,10 @@ class SystemEnv(Env): ...@@ -865,6 +876,10 @@ class SystemEnv(Env):
A system (i.e. not a virtualenv) Python environment. A system (i.e. not a virtualenv) Python environment.
""" """
@property
def sys_path(self): # type: () -> List[str]
return sys.path
def get_version_info(self): # type: () -> Tuple[int] def get_version_info(self): # type: () -> Tuple[int]
return sys.version_info return sys.version_info
...@@ -931,6 +946,13 @@ class VirtualEnv(Env): ...@@ -931,6 +946,13 @@ class VirtualEnv(Env):
if base is None: if base is None:
self._base = Path(self.run("python", "-", input_=GET_BASE_PREFIX).strip()) self._base = Path(self.run("python", "-", input_=GET_BASE_PREFIX).strip())
@property
def sys_path(self): # type: () -> List[str]
output = self.run("python", "-", input_=GET_SYS_PATH)
print(output)
return json.loads(output)
def get_version_info(self): # type: () -> Tuple[int] def get_version_info(self): # type: () -> Tuple[int]
output = self.run("python", "-", input_=GET_PYTHON_VERSION) output = self.run("python", "-", input_=GET_PYTHON_VERSION)
......
...@@ -2,9 +2,99 @@ ...@@ -2,9 +2,99 @@
import re import re
import subprocess import subprocess
from collections import namedtuple
from poetry.utils._compat import decode from poetry.utils._compat import decode
PATTERNS = [
re.compile(
r"^(git\+)?"
r"(?P<protocol>https?|git|ssh|rsync|file)://"
r"(?:(?P<user>.+)@)*"
r"(?P<resource>[a-z0-9_.-]*)"
r"(:?P<port>[\d]+)?"
r"(?P<pathname>[:/]((?P<owner>[\w\-]+)/)?"
r"((?P<name>[\w\-.]+?)(\.git|/)?)?)"
r"([@#](?P<rev>[^@#]+))?"
r"$"
),
re.compile(
r"(git\+)?"
r"((?P<protocol>\w+)://)"
r"((?P<user>\w+)@)?"
r"(?P<resource>[\w.\-]+)"
r"(:(?P<port>\d+))?"
r"(?P<pathname>(/(?P<owner>\w+)/)"
r"(/?(?P<name>[\w\-]+)(\.git|/)?)?)"
r"([@#](?P<rev>[^@#]+))?"
r"$"
),
re.compile(
r"^(?:(?P<user>.+)@)*"
r"(?P<resource>[a-z0-9_.-]*)[:]*"
r"(?P<port>[\d]+)?"
r"(?P<pathname>/?(?P<owner>.+)/(?P<name>.+).git)"
r"([@#](?P<rev>[^@#]+))?"
r"$"
),
re.compile(
r"((?P<user>\w+)@)?"
r"(?P<resource>[\w.\-]+)"
r"[:/]{1,2}"
r"(?P<pathname>((?P<owner>\w+)/)?"
r"((?P<name>[\w\-]+)(\.git|/)?)?)"
r"([@#](?P<rev>[^@#]+))?"
r"$"
),
]
class ParsedUrl:
def __init__(self, protocol, resource, pathname, user, port, name, rev):
self.protocol = protocol
self.resource = resource
self.pathname = pathname
self.user = user
self.port = port
self.name = name
self.rev = rev
@classmethod
def parse(cls, url): # type: () -> ParsedUrl
for pattern in PATTERNS:
m = pattern.match(url)
if m:
groups = m.groupdict()
return ParsedUrl(
groups.get("protocol"),
groups.get("resource"),
groups.get("pathname"),
groups.get("user"),
groups.get("port"),
groups.get("name"),
groups.get("rev"),
)
raise ValueError('Invalid git url "{}"'.format(url))
def format(self):
return "{}{}{}{}{}".format(
"{}://".format(self.protocol) if self.protocol else "",
"{}@".format(self.user) if self.user else "",
self.resource,
":{}".format(self.port) if self.port else "",
"/" + self.pathname if self.pathname.startswith(":") else self.pathname,
"#{}".format(self.rev) if self.rev else "",
)
def __str__(self): # type: () -> str
return self.format()
GitUrl = namedtuple("GitUrl", ["url", "revision"])
class GitConfig: class GitConfig:
def __init__(self, requires_git_presence=False): def __init__(self, requires_git_presence=False):
self._config = {} self._config = {}
...@@ -36,6 +126,30 @@ class Git: ...@@ -36,6 +126,30 @@ class Git:
self._config = GitConfig(requires_git_presence=True) self._config = GitConfig(requires_git_presence=True)
self._work_dir = work_dir self._work_dir = work_dir
@classmethod
def normalize_url(cls, url): # type: (str) -> GitUrl
parsed = ParsedUrl.parse(url)
formatted = re.sub(r"^git\+", "", url)
if parsed.rev:
formatted = re.sub(r"[#@]{}$".format(parsed.rev), "", formatted)
altered = parsed.format() != formatted
if altered:
if re.match(r"^git\+https?", url) and re.match(
r"^/?:[^0-9]", parsed.pathname
):
normalized = re.sub(r"git\+(.*:[^:]+):(.*)", "\\1/\\2", url)
elif re.match(r"^git\+file", url):
normalized = re.sub(r"git\+", "", url)
else:
normalized = re.sub(r"^(?:git\+)?ssh://", "", url)
else:
normalized = parsed.format()
return GitUrl(re.sub(r"#[^#]*$", "", normalized), parsed.rev)
@property @property
def config(self): # type: () -> GitConfig def config(self): # type: () -> GitConfig
return self._config return self._config
...@@ -95,7 +209,33 @@ class Git: ...@@ -95,7 +209,33 @@ class Git:
return output.split("\n") return output.split("\n")
def run(self, *args): # type: (...) -> str def remote_urls(self, folder=None): # type: (...) -> dict
output = self.run(
"config", "--get-regexp", r"remote\..*\.url", folder=folder
).strip()
urls = {}
for url in output.splitlines():
name, url = url.split(" ", 1)
urls[name.strip()] = url.strip()
return urls
def remote_url(self, folder=None): # type: (...) -> str
urls = self.remote_urls(folder=folder)
return urls.get("remote.origin.url", urls[list(urls.keys())[0]])
def run(self, *args, **kwargs): # type: (...) -> str
folder = kwargs.pop("folder", None)
if folder:
args = (
"--git-dir",
(folder / ".git").as_posix(),
"--work-tree",
folder.as_posix(),
) + args
return decode( return decode(
subprocess.check_output(["git"] + list(args), stderr=subprocess.STDOUT) subprocess.check_output(["git"] + list(args), stderr=subprocess.STDOUT)
) ).strip()
...@@ -55,6 +55,7 @@ keyring = [ ...@@ -55,6 +55,7 @@ keyring = [
] ]
# Use subprocess32 for Python 2.7 and 3.4 # Use subprocess32 for Python 2.7 and 3.4
subprocess32 = { version = "^3.5", python = "~2.7 || ~3.4" } subprocess32 = { version = "^3.5", python = "~2.7 || ~3.4" }
importlib-metadata = {version = "^0.23", python = "<=3.8"}
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
pytest = "^4.1" pytest = "^4.1"
......
...@@ -264,6 +264,42 @@ Package operations: 4 installs, 0 updates, 0 removals ...@@ -264,6 +264,42 @@ Package operations: 4 installs, 0 updates, 0 removals
} }
def test_add_git_ssh_constraint(app, repo, installer):
command = app.find("add")
tester = CommandTester(command)
repo.add_package(get_package("pendulum", "1.4.4"))
repo.add_package(get_package("cleo", "0.6.5"))
tester.execute("git+ssh://git@github.com/demo/demo.git@develop")
expected = """\
Updating dependencies
Resolving dependencies...
Writing lock file
Package operations: 2 installs, 0 updates, 0 removals
- Installing pendulum (1.4.4)
- Installing demo (0.1.2 9cf87a2)
"""
assert expected == tester.io.fetch_output()
assert len(installer.installs) == 2
content = app.poetry.file.read()["tool"]["poetry"]
assert "demo" in content["dependencies"]
assert content["dependencies"]["demo"] == {
"git": "ssh://git@github.com/demo/demo.git",
"rev": "develop",
}
def test_add_directory_constraint(app, repo, installer, mocker): def test_add_directory_constraint(app, repo, installer, mocker):
p = mocker.patch("poetry.utils._compat.Path.cwd") p = mocker.patch("poetry.utils._compat.Path.cwd")
p.return_value = Path(__file__) / ".." p.return_value = Path(__file__) / ".."
......
...@@ -17,6 +17,7 @@ from poetry.utils._compat import PY2 ...@@ -17,6 +17,7 @@ from poetry.utils._compat import PY2
from poetry.utils._compat import WINDOWS from poetry.utils._compat import WINDOWS
from poetry.utils._compat import Path from poetry.utils._compat import Path
from poetry.utils.toml_file import TomlFile from poetry.utils.toml_file import TomlFile
from poetry.vcs.git import ParsedUrl
try: try:
...@@ -32,14 +33,14 @@ def installer(): ...@@ -32,14 +33,14 @@ def installer():
def mock_clone(self, source, dest): def mock_clone(self, source, dest):
# Checking source to determine which folder we need to copy # Checking source to determine which folder we need to copy
parts = urlparse.urlparse(source) parsed = ParsedUrl.parse(source)
folder = ( folder = (
Path(__file__).parent.parent Path(__file__).parent.parent
/ "fixtures" / "fixtures"
/ "git" / "git"
/ parts.netloc / parsed.resource
/ parts.path.lstrip("/").rstrip(".git") / parsed.pathname.lstrip("/").rstrip(".git")
) )
shutil.rmtree(str(dest)) shutil.rmtree(str(dest))
......
Metadata-Version: 1.2
Name: pendulum
Version: 2.0.5
Summary: Python datetimes made easy
Home-page: https://pendulum.eustace.io
Author: Sébastien Eustace
Author-email: sebastien@eustace.io
License: UNKNOWN
Description: Pendulum
########
.. image:: https://img.shields.io/pypi/v/pendulum.svg
:target: https://pypi.python.org/pypi/pendulum
.. image:: https://img.shields.io/pypi/l/pendulum.svg
:target: https://pypi.python.org/pypi/pendulum
.. image:: https://img.shields.io/codecov/c/github/sdispater/pendulum/master.svg
:target: https://codecov.io/gh/sdispater/pendulum/branch/master
.. image:: https://travis-ci.org/sdispater/pendulum.svg
:alt: Pendulum Build status
:target: https://travis-ci.org/sdispater/pendulum
Python datetimes made easy.
Supports Python **2.7** and **3.4+**.
.. code-block:: python
>>> import pendulum
>>> now_in_paris = pendulum.now('Europe/Paris')
>>> now_in_paris
'2016-07-04T00:49:58.502116+02:00'
# Seamless timezone switching
>>> now_in_paris.in_timezone('UTC')
'2016-07-03T22:49:58.502116+00:00'
>>> tomorrow = pendulum.now().add(days=1)
>>> last_week = pendulum.now().subtract(weeks=1)
>>> past = pendulum.now().subtract(minutes=2)
>>> past.diff_for_humans()
>>> '2 minutes ago'
>>> delta = past - last_week
>>> delta.hours
23
>>> delta.in_words(locale='en')
'6 days 23 hours 58 minutes'
# Proper handling of datetime normalization
>>> pendulum.datetime(2013, 3, 31, 2, 30, tz='Europe/Paris')
'2013-03-31T03:30:00+02:00' # 2:30 does not exist (Skipped time)
# Proper handling of dst transitions
>>> just_before = pendulum.datetime(2013, 3, 31, 1, 59, 59, 999999, tz='Europe/Paris')
'2013-03-31T01:59:59.999999+01:00'
>>> just_before.add(microseconds=1)
'2013-03-31T03:00:00+02:00'
Why Pendulum?
=============
Native ``datetime`` instances are enough for basic cases but when you face more complex use-cases
they often show limitations and are not so intuitive to work with.
``Pendulum`` provides a cleaner and more easy to use API while still relying on the standard library.
So it's still ``datetime`` but better.
Unlike other datetime libraries for Python, Pendulum is a drop-in replacement
for the standard ``datetime`` class (it inherits from it), so, basically, you can replace all your ``datetime``
instances by ``DateTime`` instances in you code (exceptions exist for libraries that check
the type of the objects by using the ``type`` function like ``sqlite3`` or ``PyMySQL`` for instance).
It also removes the notion of naive datetimes: each ``Pendulum`` instance is timezone-aware
and by default in ``UTC`` for ease of use.
Pendulum also improves the standard ``timedelta`` class by providing more intuitive methods and properties.
Why not Arrow?
==============
Arrow is the most popular datetime library for Python right now, however its behavior
and API can be erratic and unpredictable. The ``get()`` method can receive pretty much anything
and it will try its best to return something while silently failing to handle some cases:
.. code-block:: python
arrow.get('2016-1-17')
# <Arrow [2016-01-01T00:00:00+00:00]>
pendulum.parse('2016-1-17')
# <Pendulum [2016-01-17T00:00:00+00:00]>
arrow.get('20160413')
# <Arrow [1970-08-22T08:06:53+00:00]>
pendulum.parse('20160413')
# <Pendulum [2016-04-13T00:00:00+00:00]>
arrow.get('2016-W07-5')
# <Arrow [2016-01-01T00:00:00+00:00]>
pendulum.parse('2016-W07-5')
# <Pendulum [2016-02-19T00:00:00+00:00]>
# Working with DST
just_before = arrow.Arrow(2013, 3, 31, 1, 59, 59, 999999, 'Europe/Paris')
just_after = just_before.replace(microseconds=1)
'2013-03-31T02:00:00+02:00'
# Should be 2013-03-31T03:00:00+02:00
(just_after.to('utc') - just_before.to('utc')).total_seconds()
-3599.999999
# Should be 1e-06
just_before = pendulum.datetime(2013, 3, 31, 1, 59, 59, 999999, 'Europe/Paris')
just_after = just_before.add(microseconds=1)
'2013-03-31T03:00:00+02:00'
(just_after.in_timezone('utc') - just_before.in_timezone('utc')).total_seconds()
1e-06
Those are a few examples showing that Arrow cannot always be trusted to have a consistent
behavior with the data you are passing to it.
Limitations
===========
Even though the ``DateTime`` class is a subclass of ``datetime`` there are some rare cases where
it can't replace the native class directly. Here is a list (non-exhaustive) of the reported cases with
a possible solution, if any:
* ``sqlite3`` will use the ``type()`` function to determine the type of the object by default. To work around it you can register a new adapter:
.. code-block:: python
from pendulum import DateTime
from sqlite3 import register_adapter
register_adapter(DateTime, lambda val: val.isoformat(' '))
* ``mysqlclient`` (former ``MySQLdb``) and ``PyMySQL`` will use the ``type()`` function to determine the type of the object by default. To work around it you can register a new adapter:
.. code-block:: python
import MySQLdb.converters
import pymysql.converters
from pendulum import DateTime
MySQLdb.converters.conversions[DateTime] = MySQLdb.converters.DateTime2literal
pymysql.converters.conversions[DateTime] = pymysql.converters.escape_datetime
* ``django`` will use the ``isoformat()`` method to store datetimes in the database. However since ``pendulum`` is always timezone aware the offset information will always be returned by ``isoformat()`` raising an error, at least for MySQL databases. To work around it you can either create your own ``DateTimeField`` or use the previous workaround for ``MySQLdb``:
.. code-block:: python
from django.db.models import DateTimeField as BaseDateTimeField
from pendulum import DateTime
class DateTimeField(BaseDateTimeField):
def value_to_string(self, obj):
val = self.value_from_object(obj)
if isinstance(value, DateTime):
return value.to_datetime_string()
return '' if val is None else val.isoformat()
Resources
=========
* `Official Website <https://pendulum.eustace.io>`_
* `Documentation <https://pendulum.eustace.io/docs/>`_
* `Issue Tracker <https://github.com/sdispater/pendulum/issues>`_
Contributing
============
Contributions are welcome, especially with localization.
Getting started
---------------
To work on the Pendulum codebase, you'll want to clone the project locally
and install the required depedendencies via `poetry <https://poetry.eustace.io>`_.
.. code-block:: bash
$ git clone git@github.com:sdispater/pendulum.git
$ poetry install
Localization
------------
If you want to help with localization, there are two different cases: the locale already exists
or not.
If the locale does not exist you will need to create it by using the ``clock`` utility:
.. code-block:: bash
./clock locale create <your-locale>
It will generate a directory in ``pendulum/locales`` named after your locale, with the following
structure:
.. code-block:: text
<your-locale>/
- custom.py
- locale.py
The ``locale.py`` file must not be modified. It contains the translations provided by
the CLDR database.
The ``custom.py`` file is the one you want to modify. It contains the data needed
by Pendulum that are not provided by the CLDR database. You can take the `en <https://github.com/sdispater/pendulum/tree/master/pendulum/locales/en/custom.py>`_
data as a reference to see which data is needed.
You should also add tests for the created or modified locale.
Platform: UNKNOWN
Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*
python-dateutil<3.0,>=2.6
pytzdata>=2018.3
[:python_version < "3.5"]
typing<4.0,>=3.6
from importlib_metadata import PathDistribution
from poetry.repositories.installed_repository import InstalledRepository from poetry.repositories.installed_repository import InstalledRepository
from poetry.utils._compat import Path
from poetry.utils.env import MockEnv as BaseMockEnv from poetry.utils.env import MockEnv as BaseMockEnv
FREEZE_RESULTS = """cleo==0.6.8 FIXTURES_DIR = Path(__file__).parent / "fixtures"
-e git+https://github.com/sdispater/pendulum.git@bb058f6b78b2d28ef5d9a5e759cfa179a1a713d6#egg=pendulum ENV_DIR = (FIXTURES_DIR / "installed").resolve()
orator===0.9.8 SITE_PACKAGES = ENV_DIR / "lib" / "python3.7" / "site-packages"
""" SRC = ENV_DIR / "src"
INSTALLED_RESULTS = [
PathDistribution(SITE_PACKAGES / "cleo-0.7.6.dist-info"),
PathDistribution(SRC / "pendulum" / "pendulum.egg-info"),
]
class MockEnv(BaseMockEnv): class MockEnv(BaseMockEnv):
def run(self, bin, *args): @property
if bin == "python" and args[:3] == ("-m", "pip", "freeze"): def site_packages(self): # type: () -> Path
return FREEZE_RESULTS return SITE_PACKAGES
return super(MockEnv, self).run(bin, *args)
def test_load(mocker):
mocker.patch(
def test_load(): "importlib_metadata.Distribution.discover", return_value=INSTALLED_RESULTS
repository = InstalledRepository.load(MockEnv()) )
mocker.patch(
assert len(repository.packages) == 3 "poetry.vcs.git.Git.rev_parse",
return_value="bb058f6b78b2d28ef5d9a5e759cfa179a1a713d6",
)
mocker.patch(
"poetry.vcs.git.Git.remote_urls",
side_effect=[
{"remote.origin.url": "https://github.com/sdispater/pendulum.git"},
{"remote.origin.url": "git@github.com:sdispater/pendulum.git"},
],
)
repository = InstalledRepository.load(MockEnv(path=ENV_DIR))
assert len(repository.packages) == 2
cleo = repository.packages[0] cleo = repository.packages[0]
assert cleo.name == "cleo" assert cleo.name == "cleo"
assert cleo.version.text == "0.6.8" assert cleo.version.text == "0.7.6"
assert (
cleo.description
== "Cleo allows you to create beautiful and testable command-line interfaces."
)
pendulum = repository.packages[1] pendulum = repository.packages[1]
assert pendulum.name == "pendulum" assert pendulum.name == "pendulum"
assert pendulum.version.text == "0.0.0" assert pendulum.version.text == "2.0.5"
assert pendulum.description == "Python datetimes made easy"
assert pendulum.source_type == "git" assert pendulum.source_type == "git"
assert pendulum.source_url == "https://github.com/sdispater/pendulum.git" assert pendulum.source_url == "https://github.com/sdispater/pendulum.git"
assert pendulum.source_reference == "bb058f6b78b2d28ef5d9a5e759cfa179a1a713d6" assert pendulum.source_reference == "bb058f6b78b2d28ef5d9a5e759cfa179a1a713d6"
orator = repository.packages[2]
assert orator.name == "orator"
assert orator.version.text == "0.9.8"
import pytest
from poetry.vcs.git import Git
from poetry.vcs.git import GitUrl
@pytest.mark.parametrize(
"url, normalized",
[
(
"git+ssh://user@hostname:project.git#commit",
GitUrl("user@hostname:project.git", "commit"),
),
(
"git+http://user@hostname/project/blah.git@commit",
GitUrl("http://user@hostname/project/blah.git", "commit"),
),
(
"git+https://user@hostname/project/blah.git",
GitUrl("https://user@hostname/project/blah.git", None),
),
(
"git+https://user@hostname:project/blah.git",
GitUrl("https://user@hostname/project/blah.git", None),
),
(
"git+ssh://git@github.com:sdispater/poetry.git#v1.0.27",
GitUrl("git@github.com:sdispater/poetry.git", "v1.0.27"),
),
(
"git+ssh://git@github.com:/sdispater/poetry.git",
GitUrl("git@github.com:/sdispater/poetry.git", None),
),
("git+ssh://git@github.com:org/repo", GitUrl("git@github.com:org/repo", None),),
(
"git+ssh://git@github.com/org/repo",
GitUrl("ssh://git@github.com/org/repo", None),
),
("git+ssh://foo:22/some/path", GitUrl("ssh://foo:22/some/path", None)),
("git@github.com:org/repo", GitUrl("git@github.com:org/repo", None)),
(
"git+https://github.com/sdispater/pendulum",
GitUrl("https://github.com/sdispater/pendulum", None),
),
(
"git+https://github.com/sdispater/pendulum#7a018f2d075b03a73409e8356f9b29c9ad4ea2c5",
GitUrl(
"https://github.com/sdispater/pendulum",
"7a018f2d075b03a73409e8356f9b29c9ad4ea2c5",
),
),
(
"git+ssh://git@git.example.com:b/b.git#v1.0.0",
GitUrl("git@git.example.com:b/b.git", "v1.0.0"),
),
(
"git+ssh://git@github.com:sdispater/pendulum.git#foo/bar",
GitUrl("git@github.com:sdispater/pendulum.git", "foo/bar"),
),
("git+file:///foo/bar.git", GitUrl("file:///foo/bar.git", None)),
(
"git+file://C:\\Users\\hello\\testing.git#zkat/windows-files",
GitUrl("file://C:\\Users\\hello\\testing.git", "zkat/windows-files"),
),
],
)
def test_normalize_url(url, normalized):
assert normalized == Git.normalize_url(url)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment