Commit d7b23405 by Sébastien Eustace

Fix error with pip 9.0.2 and change how wildcard constraints are handled

parent d27e5040
# Change Log # Change Log
[Unreleased]
### Changed
- Changed how wilcard constraints are handled.
### Fixed
- Fixed errors with pip 9.0.2.
## [0.6.1] - 2018-02-18 ## [0.6.1] - 2018-02-18
### Fixed ### Fixed
......
# -*- coding: utf-8 -*- from .utils.appdirs import user_cache_dir
from .utils.appdirs import user_config_dir
from pip.utils.appdirs import user_cache_dir, user_config_dir
CACHE_DIR = user_cache_dir('pypoetry') CACHE_DIR = user_cache_dir('pypoetry')
......
import os
import re
from poetry.semver.version_parser import VersionParser
from poetry.version.markers import Marker
from poetry.version.requirements import Requirement
from .dependency import Dependency from .dependency import Dependency
from .locker import Locker from .locker import Locker
from .package import Package from .package import Package
from .utils.link import Link
from .utils.utils import convert_markers
from .utils.utils import group_markers
from .utils.utils import is_archive_file
from .utils.utils import is_installable_dir
from .utils.utils import is_url
from .utils.utils import path_to_url
from .utils.utils import strip_extras
from .vcs_dependency import VCSDependency from .vcs_dependency import VCSDependency
def dependency_from_pep_508(name):
req = Requirement(name)
if req.marker:
markers = convert_markers(req.marker.markers)
else:
markers = {}
name = req.name
path = os.path.normpath(os.path.abspath(name))
link = None
if is_url(name):
link = Link(name)
else:
p, extras = strip_extras(path)
if (os.path.isdir(p) and
(os.path.sep in name or name.startswith('.'))):
if not is_installable_dir(p):
raise ValueError(
"Directory %r is not installable. File 'setup.py' "
"not found." % name
)
link = Link(path_to_url(p))
elif is_archive_file(p):
link = Link(path_to_url(p))
# it's a local file, dir, or url
if link:
# Handle relative file URLs
if link.scheme == 'file' and re.search(r'\.\./', link.url):
link = Link(
path_to_url(os.path.normpath(os.path.abspath(link.path)))
)
# wheel file
if link.is_wheel:
m = re.match(
'^(?P<namever>(?P<name>.+?)-(?P<ver>\d.*?))',
link.filename
)
if not m:
raise ValueError(f'Invalid wheel name: {link.filename}')
name = m.group('name')
version = m.group('ver')
dep = Dependency(name, version)
else:
name = link.egg_fragment
if link.scheme == 'git':
dep = VCSDependency(name, 'git', link.url_without_fragment)
else:
dep = Dependency(name, '*')
else:
if req.pretty_constraint:
constraint = req.constraint
else:
constraint = '*'
dep = Dependency(name, constraint)
if 'extra' in markers:
# If we have extras, the dependency is optional
dep.deactivate()
for or_ in markers['extra']:
for _, extra in or_:
dep.extras.append(extra)
if 'python_version' in markers:
ors = []
for or_ in markers['python_version']:
ands = []
for op, version in or_:
# Expand python version
if op == '==':
version = '~' + version
op = ''
elif op == '!=':
version += '.*'
ands.append(f'{op}{version}')
ors.append(' '.join(ands))
dep.python_versions = ' || '.join(ors)
if 'sys_platform' in markers:
ors = []
for or_ in markers['sys_platform']:
ands = []
for op, platform in or_:
if op == '==':
op = ''
ands.append(f'{op}{platform}')
ors.append(' '.join(ands))
dep.platform = ' || '.join(ors)
return dep
...@@ -3,6 +3,7 @@ import poetry.packages ...@@ -3,6 +3,7 @@ import poetry.packages
from poetry.semver.constraints import Constraint from poetry.semver.constraints import Constraint
from poetry.semver.constraints import EmptyConstraint from poetry.semver.constraints import EmptyConstraint
from poetry.semver.constraints import MultiConstraint from poetry.semver.constraints import MultiConstraint
from poetry.semver.constraints.base_constraint import BaseConstraint
from poetry.semver.version_parser import VersionParser from poetry.semver.version_parser import VersionParser
from .constraints.platform_constraint import PlatformConstraint from .constraints.platform_constraint import PlatformConstraint
...@@ -21,7 +22,10 @@ class Dependency: ...@@ -21,7 +22,10 @@ class Dependency:
self._parser = VersionParser() self._parser = VersionParser()
try: try:
if not isinstance(constraint, BaseConstraint):
self._constraint = self._parser.parse_constraints(constraint) self._constraint = self._parser.parse_constraints(constraint)
else:
self._constraint = constraint
except ValueError: except ValueError:
self._constraint = self._parser.parse_constraints('*') self._constraint = self._parser.parse_constraints('*')
...@@ -130,6 +134,10 @@ class Dependency: ...@@ -130,6 +134,10 @@ class Dependency:
return requirement return requirement
@classmethod
def from_pep_508(cls):
return
def _create_nested_marker(self, name, constraint): def _create_nested_marker(self, name, constraint):
if isinstance(constraint, MultiConstraint): if isinstance(constraint, MultiConstraint):
parts = [] parts = []
......
import posixpath
import urllib.parse
import urllib.request
import re
from .utils import path_to_url
from .utils import splitext
class Link:
def __init__(self, url, comes_from=None, requires_python=None):
"""
Object representing a parsed link from https://pypi.python.org/simple/*
url:
url of the resource pointed to (href of the link)
comes_from:
instance of HTMLPage where the link was found, or string.
requires_python:
String containing the `Requires-Python` metadata field, specified
in PEP 345. This may be specified by a data-requires-python
attribute in the HTML link tag, as described in PEP 503.
"""
# url can be a UNC windows share
if url.startswith('\\\\'):
url = path_to_url(url)
self.url = url
self.comes_from = comes_from
self.requires_python = requires_python if requires_python else None
def __str__(self):
if self.requires_python:
rp = ' (requires-python:%s)' % self.requires_python
else:
rp = ''
if self.comes_from:
return '%s (from %s)%s' % (self.url, self.comes_from, rp)
else:
return str(self.url)
def __repr__(self):
return '<Link %s>' % self
def __eq__(self, other):
if not isinstance(other, Link):
return NotImplemented
return self.url == other.url
def __ne__(self, other):
if not isinstance(other, Link):
return NotImplemented
return self.url != other.url
def __lt__(self, other):
if not isinstance(other, Link):
return NotImplemented
return self.url < other.url
def __le__(self, other):
if not isinstance(other, Link):
return NotImplemented
return self.url <= other.url
def __gt__(self, other):
if not isinstance(other, Link):
return NotImplemented
return self.url > other.url
def __ge__(self, other):
if not isinstance(other, Link):
return NotImplemented
return self.url >= other.url
def __hash__(self):
return hash(self.url)
@property
def filename(self):
_, netloc, path, _, _ = urllib.parse.urlsplit(self.url)
name = posixpath.basename(path.rstrip('/')) or netloc
name = urllib.parse.unquote(name)
assert name, ('URL %r produced no filename' % self.url)
return name
@property
def scheme(self):
return urllib.parse.urlsplit(self.url)[0]
@property
def netloc(self):
return urllib.parse.urlsplit(self.url)[1]
@property
def path(self):
return urllib.parse.unquote(urllib.parse.urlsplit(self.url)[2])
def splitext(self):
return splitext(posixpath.basename(self.path.rstrip('/')))
@property
def ext(self):
return self.splitext()[1]
@property
def url_without_fragment(self):
scheme, netloc, path, query, fragment = urllib.parse.urlsplit(self.url)
return urllib.parse.urlunsplit((scheme, netloc, path, query, None))
_egg_fragment_re = re.compile(r'[#&]egg=([^&]*)')
@property
def egg_fragment(self):
match = self._egg_fragment_re.search(self.url)
if not match:
return None
return match.group(1)
_subdirectory_fragment_re = re.compile(r'[#&]subdirectory=([^&]*)')
@property
def subdirectory_fragment(self):
match = self._subdirectory_fragment_re.search(self.url)
if not match:
return None
return match.group(1)
_hash_re = re.compile(
r'(sha1|sha224|sha384|sha256|sha512|md5)=([a-f0-9]+)'
)
@property
def hash(self):
match = self._hash_re.search(self.url)
if match:
return match.group(2)
return None
@property
def hash_name(self):
match = self._hash_re.search(self.url)
if match:
return match.group(1)
return None
@property
def show_url(self):
return posixpath.basename(self.url.split('#', 1)[0].split('?', 1)[0])
@property
def is_wheel(self):
return self.ext == '.whl'
@property
def is_artifact(self):
"""
Determines if this points to an actual artifact (e.g. a tarball) or if
it points to an "abstract" thing like a path or a VCS location.
"""
if self.scheme in ['ssh', 'git', 'hg', 'bzr', 'sftp', 'svn']:
return False
return True
import os
import posixpath
import re
import urllib.parse
import urllib.request
BZ2_EXTENSIONS = ('.tar.bz2', '.tbz')
XZ_EXTENSIONS = ('.tar.xz', '.txz', '.tlz', '.tar.lz', '.tar.lzma')
ZIP_EXTENSIONS = ('.zip', '.whl')
TAR_EXTENSIONS = ('.tar.gz', '.tgz', '.tar')
ARCHIVE_EXTENSIONS = (
ZIP_EXTENSIONS + BZ2_EXTENSIONS + TAR_EXTENSIONS + XZ_EXTENSIONS)
SUPPORTED_EXTENSIONS = ZIP_EXTENSIONS + TAR_EXTENSIONS
try:
import bz2 # noqa
SUPPORTED_EXTENSIONS += BZ2_EXTENSIONS
except ImportError:
pass
try:
# Only for Python 3.3+
import lzma # noqa
SUPPORTED_EXTENSIONS += XZ_EXTENSIONS
except ImportError:
pass
def path_to_url(path):
"""
Convert a path to a file: URL. The path will be made absolute and have
quoted path parts.
"""
path = os.path.normpath(os.path.abspath(path))
url = urllib.parse.urljoin('file:', urllib.request.pathname2url(path))
return url
def is_url(name):
if ':' not in name:
return False
scheme = name.split(':', 1)[0].lower()
return scheme in [
'http', 'https',
'file',
'ftp',
'ssh', 'git', 'hg', 'bzr', 'sftp', 'svn'
'ssh'
]
def strip_extras(path):
m = re.match(r'^(.+)(\[[^\]]+\])$', path)
extras = None
if m:
path_no_extras = m.group(1)
extras = m.group(2)
else:
path_no_extras = path
return path_no_extras, extras
def is_installable_dir(path):
"""Return True if `path` is a directory containing a setup.py file."""
if not os.path.isdir(path):
return False
setup_py = os.path.join(path, 'setup.py')
if os.path.isfile(setup_py):
return True
return False
def is_archive_file(name):
"""Return True if `name` is a considered as an archive file."""
ext = splitext(name)[1].lower()
if ext in ARCHIVE_EXTENSIONS:
return True
return False
def splitext(path):
"""Like os.path.splitext, but take off .tar too"""
base, ext = posixpath.splitext(path)
if base.lower().endswith('.tar'):
ext = base[-4:] + ext
base = base[:-4]
return base, ext
def group_markers(markers):
groups = [[]]
for marker in markers:
assert isinstance(marker, (list, tuple, str))
if isinstance(marker, list):
groups[-1].append(group_markers(marker))
elif isinstance(marker, tuple):
lhs, op, rhs = marker
groups[-1].append((lhs.value, op, rhs.value))
else:
assert marker in ["and", "or"]
if marker == "or":
groups.append([])
return groups
def convert_markers(markers):
groups = group_markers(markers)
requirements = {}
def _group(_groups, or_=False):
nonlocal requirements
for group in _groups:
if isinstance(group, tuple):
variable, op, value = group
group_name = str(variable)
if group_name not in requirements:
requirements[group_name] = [[]]
elif or_:
requirements[group_name].append([])
or_ = False
requirements[group_name][-1].append((str(op), str(value)))
else:
_group(group, or_=True)
_group(groups)
return requirements
import re
from pathlib import Path from pathlib import Path
from piptools.cache import DependencyCache from piptools.cache import DependencyCache
from piptools.repositories import PyPIRepository from piptools.repositories import PyPIRepository
from piptools.resolver import Resolver from piptools.resolver import Resolver
from piptools.scripts.compile import get_pip_command from piptools.scripts.compile import get_pip_command
from pip.req import InstallRequirement
from pip.exceptions import InstallationError
from cachy import CacheManager from cachy import CacheManager
import poetry.packages import poetry.packages
from poetry.locations import CACHE_DIR from poetry.locations import CACHE_DIR
from poetry.packages import dependency_from_pep_508
from poetry.semver.constraints import Constraint from poetry.semver.constraints import Constraint
from poetry.semver.constraints.base_constraint import BaseConstraint from poetry.semver.constraints.base_constraint import BaseConstraint
from poetry.semver.version_parser import VersionParser from poetry.semver.version_parser import VersionParser
from poetry.version.markers import InvalidMarker
from .pypi_repository import PyPiRepository from .pypi_repository import PyPiRepository
...@@ -111,56 +109,23 @@ class LegacyRepository(PyPiRepository): ...@@ -111,56 +109,23 @@ class LegacyRepository(PyPiRepository):
release_info = self.get_release_info(name, version) release_info = self.get_release_info(name, version)
package = poetry.packages.Package(name, version, version) package = poetry.packages.Package(name, version, version)
for req in release_info['requires_dist']: for req in release_info['requires_dist']:
req = InstallRequirement.from_line(req) try:
dependency = dependency_from_pep_508(req)
name = req.name except InvalidMarker:
version = str(req.req.specifier) # Invalid marker
# We strip the markers hoping for the best
dependency = Dependency( req = req.split(';')[0]
name,
version,
optional=req.markers
)
is_extra = False
if req.markers:
# Setting extra dependencies and requirements
requirements = self._convert_markers(
req.markers._markers
)
if 'python_version' in requirements:
ors = []
for or_ in requirements['python_version']:
ands = []
for op, version in or_:
ands.append(f'{op}{version}')
ors.append(' '.join(ands))
dependency.python_versions = ' || '.join(ors)
if 'sys_platform' in requirements:
ors = []
for or_ in requirements['sys_platform']:
ands = []
for op, platform in or_:
ands.append(f'{op}{platform}')
ors.append(' '.join(ands))
dependency.platform = ' || '.join(ors) dependency = dependency_from_pep_508(req)
if 'extra' in requirements: if dependency.extras:
is_extra = True for extra in dependency.extras:
for _extras in requirements['extra']:
for _, extra in _extras:
if extra not in package.extras: if extra not in package.extras:
package.extras[extra] = [] package.extras[extra] = []
package.extras[extra].append(dependency) package.extras[extra].append(dependency)
if not is_extra: if not dependency.is_optional():
package.requires.append(dependency) package.requires.append(dependency)
# Adding description # Adding description
...@@ -194,6 +159,9 @@ class LegacyRepository(PyPiRepository): ...@@ -194,6 +159,9 @@ class LegacyRepository(PyPiRepository):
) )
def _get_release_info(self, name: str, version: str) -> dict: def _get_release_info(self, name: str, version: str) -> dict:
from pip.req import InstallRequirement
from pip.exceptions import InstallationError
ireq = InstallRequirement.from_line(f'{name}=={version}') ireq = InstallRequirement.from_line(f'{name}=={version}')
resolver = Resolver( resolver = Resolver(
[ireq], self._repository, [ireq], self._repository,
......
from pathlib import Path from pathlib import Path
from pip.req import InstallRequirement
from typing import List from typing import List
from typing import Union from typing import Union
...@@ -7,11 +6,12 @@ from cachy import CacheManager ...@@ -7,11 +6,12 @@ from cachy import CacheManager
from requests import get from requests import get
from poetry.locations import CACHE_DIR from poetry.locations import CACHE_DIR
from poetry.packages import Dependency from poetry.packages import dependency_from_pep_508
from poetry.packages import Package from poetry.packages import Package
from poetry.semver.constraints import Constraint from poetry.semver.constraints import Constraint
from poetry.semver.constraints.base_constraint import BaseConstraint from poetry.semver.constraints.base_constraint import BaseConstraint
from poetry.semver.version_parser import VersionParser from poetry.semver.version_parser import VersionParser
from poetry.version.markers import InvalidMarker
from .repository import Repository from .repository import Repository
...@@ -86,57 +86,16 @@ class PyPiRepository(Repository): ...@@ -86,57 +86,16 @@ class PyPiRepository(Repository):
requires_dist = release_info['requires_dist'] or [] requires_dist = release_info['requires_dist'] or []
for req in requires_dist: for req in requires_dist:
try: try:
req = InstallRequirement.from_line(req) dependency = dependency_from_pep_508(req)
except Exception: except InvalidMarker:
# Probably an invalid marker # Invalid marker
# We strip the markers hoping for the best # We strip the markers hoping for the best
req = req.split(';')[0] req = req.split(';')[0]
req = InstallRequirement.from_line(req) dependency = dependency_from_pep_508(req)
name = req.name if dependency.extras:
version = str(req.req.specifier) for extra in dependency.extras:
dependency = Dependency(
name,
version
)
if req.markers:
# Setting extra dependencies and requirements
requirements = self._convert_markers(
req.markers._markers
)
if 'python_version' in requirements:
ors = []
for or_ in requirements['python_version']:
ands = []
for op, version in or_:
ands.append(f'{op}{version}')
ors.append(' '.join(ands))
dependency.python_versions = ' || '.join(ors)
if 'sys_platform' in requirements:
ors = []
for or_ in requirements['sys_platform']:
ands = []
for op, platform in or_:
if op == '==':
op = ''
ands.append(f'{op}{platform}')
ors.append(' '.join(ands))
dependency.platform = ' || '.join(ors)
if 'extra' in requirements:
dependency.deactivate()
for _extras in requirements['extra']:
for _, extra in _extras:
if extra not in package.extras: if extra not in package.extras:
package.extras[extra] = [] package.extras[extra] = []
...@@ -250,47 +209,3 @@ class PyPiRepository(Repository): ...@@ -250,47 +209,3 @@ class PyPiRepository(Repository):
json_data = json_response.json() json_data = json_response.json()
return json_data return json_data
def _group_markers(self, markers):
groups = [[]]
for marker in markers:
assert isinstance(marker, (list, tuple, str))
if isinstance(marker, list):
groups[-1].append(self._group_markers(marker))
elif isinstance(marker, tuple):
lhs, op, rhs = marker
groups[-1].append((lhs.value, op, rhs.value))
else:
assert marker in ["and", "or"]
if marker == "or":
groups.append([])
return groups
def _convert_markers(self, markers):
groups = self._group_markers(markers)[0]
requirements = {}
def _group(_groups, or_=False):
nonlocal requirements
for group in _groups:
if isinstance(group, tuple):
variable, op, value = group
group_name = str(variable)
if group_name not in requirements:
requirements[group_name] = [[]]
elif or_:
requirements[group_name].append([])
requirements[group_name][-1].append((str(op), str(value)))
else:
_group(group, or_=True)
_group(groups)
return requirements
...@@ -36,7 +36,7 @@ class Constraint(BaseConstraint): ...@@ -36,7 +36,7 @@ class Constraint(BaseConstraint):
} }
def __init__(self, operator: str, version: str): def __init__(self, operator: str, version: str):
if operator not in self._trans_op_str: if operator not in self.supported_operators:
raise ValueError( raise ValueError(
f'Invalid operator "{operator}" given, ' f'Invalid operator "{operator}" given, '
f'expected one of: {", ".join(self.supported_operators)}' f'expected one of: {", ".join(self.supported_operators)}'
...@@ -63,7 +63,10 @@ class Constraint(BaseConstraint): ...@@ -63,7 +63,10 @@ class Constraint(BaseConstraint):
return self._version return self._version
def matches(self, provider): def matches(self, provider):
if isinstance(provider, self.__class__): if (
isinstance(provider, self.__class__)
and provider.__class__ is self.__class__
):
return self.match_specific(provider) return self.match_specific(provider)
# turn matching around to find a match # turn matching around to find a match
......
import re
from .constraint import Constraint
class WilcardConstraint(Constraint):
def __init__(self, constraint: str):
m = re.match(
'^(!=|==)?v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.[xX*])+$',
constraint
)
if not m:
raise ValueError('Invalid value for wildcard constraint')
if not m.group(1):
operator = '=='
else:
operator = m.group(1)
super().__init__(
operator,
'.'.join([g if g else '*' for g in m.groups()[1:]])
)
if m.group(4):
position = 2
elif m.group(3):
position = 1
else:
position = 0
from ..version_parser import VersionParser
parser = VersionParser()
groups = m.groups()[1:]
low_version = parser._manipulate_version_string(
groups, position
)
high_version = parser._manipulate_version_string(
groups, position, 1
)
if operator == '!=':
if low_version == '0.0.0.0':
self._constraint = Constraint('>=', high_version)
else:
self._constraint = parser.parse_constraints(
f'<{low_version} || >={high_version}'
)
else:
if low_version == '0.0.0.0':
self._constraint = Constraint('<', high_version)
else:
self._constraint = parser.parse_constraints(
f'>={low_version},<{high_version}'
)
@property
def supported_operators(self):
return ['!=', '==']
@property
def constraint(self):
return self._constraint
def matches(self, provider) -> bool:
if isinstance(provider, self.__class__):
return self._constraint.matches(provider.constraint)
return provider.matches(self._constraint)
def __str__(self):
op = ''
if self.string_operator == '!=':
op = '!= '
return '{}{}'.format(
op,
self._version
)
...@@ -3,6 +3,7 @@ import re ...@@ -3,6 +3,7 @@ import re
from .constraints.constraint import Constraint from .constraints.constraint import Constraint
from .constraints.empty_constraint import EmptyConstraint from .constraints.empty_constraint import EmptyConstraint
from .constraints.multi_constraint import MultiConstraint from .constraints.multi_constraint import MultiConstraint
from .constraints.wildcard_constraint import WilcardConstraint
from .helpers import normalize_version, _expand_stability from .helpers import normalize_version, _expand_stability
...@@ -103,7 +104,7 @@ class VersionParser: ...@@ -103,7 +104,7 @@ class VersionParser:
# to ensure that unstable instances of the current version are allowed. # to ensure that unstable instances of the current version are allowed.
# However, if a stability suffix is added to the constraint, # However, if a stability suffix is added to the constraint,
# then a >= match on the current version is used instead. # then a >= match on the current version is used instead.
m = re.match('(?i)^~{}$'.format(version_regex), constraint) m = re.match('(?i)^~=?{}$'.format(version_regex), constraint)
if m: if m:
# Work out which position in the version we are operating at # Work out which position in the version we are operating at
if m.group(4): if m.group(4):
...@@ -176,35 +177,12 @@ class VersionParser: ...@@ -176,35 +177,12 @@ class VersionParser:
# A partial version range is treated as an X-Range, # A partial version range is treated as an X-Range,
# so the special character is in fact optional. # so the special character is in fact optional.
m = re.match( m = re.match(
'^(!=)?v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.[xX*])+$', '^(!=|==)?v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.[xX*])+$',
constraint constraint
) )
if m: if m:
if m.group(4): # We just leave it as is
position = 2 return WilcardConstraint(constraint),
elif m.group(3):
position = 1
else:
position = 0
groups = m.groups()[1:]
low_version = self._manipulate_version_string(
groups, position
)
high_version = self._manipulate_version_string(
groups, position, 1
)
if m.group(1):
if low_version == '0.0.0.0':
return Constraint('>=', high_version),
return self.parse_constraints(f'<{low_version} || >={high_version}'),
if low_version == '0.0.0.0':
return Constraint('<', high_version),
return Constraint('>=', low_version), Constraint('<', high_version)
# Basic Comparators # Basic Comparators
m = re.match('^(<>|!=|>=?|<=?|==?)?\s*(.*)', constraint) m = re.match('^(<>|!=|>=?|<=?|==?)?\s*(.*)', constraint)
......
"""
This code was taken from https://github.com/ActiveState/appdirs and modified
to suit our purposes.
"""
import os
import sys
WINDOWS = (sys.platform.startswith("win") or
(sys.platform == 'cli' and os.name == 'nt'))
def expanduser(path):
"""
Expand ~ and ~user constructions.
Includes a workaround for http://bugs.python.org/issue14768
"""
expanded = os.path.expanduser(path)
if path.startswith('~/') and expanded.startswith('//'):
expanded = expanded[1:]
return expanded
def user_cache_dir(appname):
r"""
Return full path to the user-specific cache dir for this application.
"appname" is the name of application.
Typical user cache directories are:
macOS: ~/Library/Caches/<AppName>
Unix: ~/.cache/<AppName> (XDG default)
Windows: C:\Users\<username>\AppData\Local\<AppName>\Cache
On Windows the only suggestion in the MSDN docs is that local settings go
in the `CSIDL_LOCAL_APPDATA` directory. This is identical to the
non-roaming app data dir (the default returned by `user_data_dir`). Apps
typically put cache data somewhere *under* the given dir here. Some
examples:
...\Mozilla\Firefox\Profiles\<ProfileName>\Cache
...\Acme\SuperApp\Cache\1.0
OPINION: This function appends "Cache" to the `CSIDL_LOCAL_APPDATA` value.
"""
if WINDOWS:
# Get the base path
path = os.path.normpath(_get_win_folder("CSIDL_LOCAL_APPDATA"))
# Add our app name and Cache directory to it
path = os.path.join(path, appname, "Cache")
elif sys.platform == "darwin":
# Get the base path
path = expanduser("~/Library/Caches")
# Add our app name to it
path = os.path.join(path, appname)
else:
# Get the base path
path = os.getenv("XDG_CACHE_HOME", expanduser("~/.cache"))
# Add our app name to it
path = os.path.join(path, appname)
return path
def user_data_dir(appname, roaming=False):
"""
Return full path to the user-specific data dir for this application.
"appname" is the name of application.
If None, just the system directory is returned.
"roaming" (boolean, default False) can be set True to use the Windows
roaming appdata directory. That means that for users on a Windows
network setup for roaming profiles, this user data will be
sync'd on login. See
<http://technet.microsoft.com/en-us/library/cc766489(WS.10).aspx>
for a discussion of issues.
Typical user data directories are:
macOS: ~/Library/Application Support/<AppName>
Unix: ~/.local/share/<AppName> # or in
$XDG_DATA_HOME, if defined
Win XP (not roaming): C:\Documents and Settings\<username>\ ...
...Application Data\<AppName>
Win XP (roaming): C:\Documents and Settings\<username>\Local ...
...Settings\Application Data\<AppName>
Win 7 (not roaming): C:\\Users\<username>\AppData\Local\<AppName>
Win 7 (roaming): C:\\Users\<username>\AppData\Roaming\<AppName>
For Unix, we follow the XDG spec and support $XDG_DATA_HOME.
That means, by default "~/.local/share/<AppName>".
"""
if WINDOWS:
const = roaming and "CSIDL_APPDATA" or "CSIDL_LOCAL_APPDATA"
path = os.path.join(os.path.normpath(_get_win_folder(const)), appname)
elif sys.platform == "darwin":
path = os.path.join(
expanduser('~/Library/Application Support/'),
appname,
)
else:
path = os.path.join(
os.getenv('XDG_DATA_HOME', expanduser("~/.local/share")),
appname,
)
return path
def user_config_dir(appname, roaming=True):
"""Return full path to the user-specific config dir for this application.
"appname" is the name of application.
If None, just the system directory is returned.
"roaming" (boolean, default True) can be set False to not use the
Windows roaming appdata directory. That means that for users on a
Windows network setup for roaming profiles, this user data will be
sync'd on login. See
<http://technet.microsoft.com/en-us/library/cc766489(WS.10).aspx>
for a discussion of issues.
Typical user data directories are:
macOS: same as user_data_dir
Unix: ~/.config/<AppName>
Win *: same as user_data_dir
For Unix, we follow the XDG spec and support $XDG_CONFIG_HOME.
That means, by default "~/.config/<AppName>".
"""
if WINDOWS:
path = user_data_dir(appname, roaming=roaming)
elif sys.platform == "darwin":
path = user_data_dir(appname)
else:
path = os.getenv('XDG_CONFIG_HOME', expanduser("~/.config"))
path = os.path.join(path, appname)
return path
# for the discussion regarding site_config_dirs locations
# see <https://github.com/pypa/pip/issues/1733>
def site_config_dirs(appname):
"""Return a list of potential user-shared config dirs for this application.
"appname" is the name of application.
Typical user config directories are:
macOS: /Library/Application Support/<AppName>/
Unix: /etc or $XDG_CONFIG_DIRS[i]/<AppName>/ for each value in
$XDG_CONFIG_DIRS
Win XP: C:\Documents and Settings\All Users\Application ...
...Data\<AppName>\
Vista: (Fail! "C:\ProgramData" is a hidden *system* directory
on Vista.)
Win 7: Hidden, but writeable on Win 7:
C:\ProgramData\<AppName>\
"""
if WINDOWS:
path = os.path.normpath(_get_win_folder("CSIDL_COMMON_APPDATA"))
pathlist = [os.path.join(path, appname)]
elif sys.platform == 'darwin':
pathlist = [os.path.join('/Library/Application Support', appname)]
else:
# try looking in $XDG_CONFIG_DIRS
xdg_config_dirs = os.getenv('XDG_CONFIG_DIRS', '/etc/xdg')
if xdg_config_dirs:
pathlist = [
os.path.join(expanduser(x), appname)
for x in xdg_config_dirs.split(os.pathsep)
]
else:
pathlist = []
# always look in /etc directly as well
pathlist.append('/etc')
return pathlist
# -- Windows support functions --
def _get_win_folder_from_registry(csidl_name):
"""
This is a fallback technique at best. I'm not sure if using the
registry for this guarantees us the correct answer for all CSIDL_*
names.
"""
import _winreg
shell_folder_name = {
"CSIDL_APPDATA": "AppData",
"CSIDL_COMMON_APPDATA": "Common AppData",
"CSIDL_LOCAL_APPDATA": "Local AppData",
}[csidl_name]
key = _winreg.OpenKey(
_winreg.HKEY_CURRENT_USER,
r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
)
directory, _type = _winreg.QueryValueEx(key, shell_folder_name)
return directory
def _get_win_folder_with_ctypes(csidl_name):
csidl_const = {
"CSIDL_APPDATA": 26,
"CSIDL_COMMON_APPDATA": 35,
"CSIDL_LOCAL_APPDATA": 28,
}[csidl_name]
buf = ctypes.create_unicode_buffer(1024)
ctypes.windll.shell32.SHGetFolderPathW(None, csidl_const, None, 0, buf)
# Downgrade to short path name if have highbit chars. See
# <http://bugs.activestate.com/show_bug.cgi?id=85099>.
has_high_char = False
for c in buf:
if ord(c) > 255:
has_high_char = True
break
if has_high_char:
buf2 = ctypes.create_unicode_buffer(1024)
if ctypes.windll.kernel32.GetShortPathNameW(buf.value, buf2, 1024):
buf = buf2
return buf.value
if WINDOWS:
try:
import ctypes
_get_win_folder = _get_win_folder_with_ctypes
except ImportError:
_get_win_folder = _get_win_folder_from_registry
def _win_path_to_bytes(path):
"""Encode Windows paths to bytes. Only used on Python 2.
Motivation is to be consistent with other operating systems where paths
are also returned as bytes. This avoids problems mixing bytes and Unicode
elsewhere in the codebase. For more details and discussion see
<https://github.com/pypa/pip/issues/3463>.
If encoding using ASCII and MBCS fails, return the original Unicode path.
"""
for encoding in ('ASCII', 'MBCS'):
try:
return path.encode(encoding)
except (UnicodeEncodeError, LookupError):
pass
return path
from pip import __version__
from poetry.version import parse
if parse(__version__) >= parse('9.0.2'):
from pip._internal.req import InstallRequirement
from pip._internal.utils.appdirs import user_cache_dir, user_config_dir
else:
from pip.req import InstallRequirement
from pip.utils.appdirs import user_cache_dir, user_config_dir
...@@ -30,7 +30,8 @@ def format_python_constraint(constraint): ...@@ -30,7 +30,8 @@ def format_python_constraint(constraint):
return str(constraint) return str(constraint)
for version in PYTHON_VERSION: for version in PYTHON_VERSION:
matches = constraint.matches(parser.parse_constraints(version)) version_constraint = parser.parse_constraints(version)
matches = constraint.matches(version_constraint)
if not matches: if not matches:
formatted.append('!=' + version) formatted.append('!=' + version)
else: else:
......
import operator
from pyparsing import (
ParseException, ParseResults, stringStart, stringEnd,
)
from pyparsing import ZeroOrMore, Group, Forward, QuotedString
from pyparsing import Literal as L # noqa
class InvalidMarker(ValueError):
"""
An invalid marker was found, users should refer to PEP 508.
"""
class UndefinedComparison(ValueError):
"""
An invalid operation was attempted on a value that doesn't support it.
"""
class UndefinedEnvironmentName(ValueError):
"""
A name was attempted to be used that does not exist inside of the
environment.
"""
class Node(object):
def __init__(self, value):
self.value = value
def __str__(self):
return str(self.value)
def __repr__(self):
return "<{0}({1!r})>".format(self.__class__.__name__, str(self))
def serialize(self):
raise NotImplementedError
class Variable(Node):
def serialize(self):
return str(self)
class Value(Node):
def serialize(self):
return '"{0}"'.format(self)
class Op(Node):
def serialize(self):
return str(self)
VARIABLE = (
L("implementation_version") |
L("platform_python_implementation") |
L("implementation_name") |
L("python_full_version") |
L("platform_release") |
L("platform_version") |
L("platform_machine") |
L("platform_system") |
L("python_version") |
L("sys_platform") |
L("os_name") |
L("os.name") | # PEP-345
L("sys.platform") | # PEP-345
L("platform.version") | # PEP-345
L("platform.machine") | # PEP-345
L("platform.python_implementation") | # PEP-345
L("python_implementation") | # undocumented setuptools legacy
L("extra")
)
ALIASES = {
'os.name': 'os_name',
'sys.platform': 'sys_platform',
'platform.version': 'platform_version',
'platform.machine': 'platform_machine',
'platform.python_implementation': 'platform_python_implementation',
'python_implementation': 'platform_python_implementation'
}
VARIABLE.setParseAction(lambda s, l, t: Variable(ALIASES.get(t[0], t[0])))
VERSION_CMP = (
L("===") |
L("==") |
L(">=") |
L("<=") |
L("!=") |
L("~=") |
L(">") |
L("<")
)
MARKER_OP = VERSION_CMP | L("not in") | L("in")
MARKER_OP.setParseAction(lambda s, l, t: Op(t[0]))
MARKER_VALUE = QuotedString("'") | QuotedString('"')
MARKER_VALUE.setParseAction(lambda s, l, t: Value(t[0]))
BOOLOP = L("and") | L("or")
MARKER_VAR = VARIABLE | MARKER_VALUE
MARKER_ITEM = Group(MARKER_VAR + MARKER_OP + MARKER_VAR)
MARKER_ITEM.setParseAction(lambda s, l, t: tuple(t[0]))
LPAREN = L("(").suppress()
RPAREN = L(")").suppress()
MARKER_EXPR = Forward()
MARKER_ATOM = MARKER_ITEM | Group(LPAREN + MARKER_EXPR + RPAREN)
MARKER_EXPR << MARKER_ATOM + ZeroOrMore(BOOLOP + MARKER_EXPR)
MARKER = stringStart + MARKER_EXPR + stringEnd
def _coerce_parse_result(results):
if isinstance(results, ParseResults):
return [_coerce_parse_result(i) for i in results]
else:
return results
def _format_marker(marker, first=True):
assert isinstance(marker, (list, tuple, str))
# Sometimes we have a structure like [[...]] which is a single item list
# where the single item is itself it's own list. In that case we want skip
# the rest of this function so that we don't get extraneous () on the
# outside.
if (isinstance(marker, list) and len(marker) == 1 and
isinstance(marker[0], (list, tuple))):
return _format_marker(marker[0])
if isinstance(marker, list):
inner = (_format_marker(m, first=False) for m in marker)
if first:
return " ".join(inner)
else:
return "(" + " ".join(inner) + ")"
elif isinstance(marker, tuple):
return " ".join([m.serialize() for m in marker])
else:
return marker
_operators = {
"in": lambda lhs, rhs: lhs in rhs,
"not in": lambda lhs, rhs: lhs not in rhs,
"<": operator.lt,
"<=": operator.le,
"==": operator.eq,
"!=": operator.ne,
">=": operator.ge,
">": operator.gt,
}
def format_full_version(info):
version = '{0.major}.{0.minor}.{0.micro}'.format(info)
kind = info.releaselevel
if kind != 'final':
version += kind[0] + str(info.serial)
return version
class Marker(object):
def __init__(self, marker):
try:
self._markers = _coerce_parse_result(MARKER.parseString(marker))
except ParseException as e:
err_str = "Invalid marker: {0!r}, parse error at {1!r}".format(
marker, marker[e.loc:e.loc + 8])
raise InvalidMarker(err_str)
@property
def markers(self):
return self._markers
def __str__(self):
return _format_marker(self._markers)
def __repr__(self):
return "<Marker({0!r})>".format(str(self))
# This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details.
from __future__ import absolute_import, division, print_function
import string
import re
import urllib.parse as urlparse
from pyparsing import (
stringStart, stringEnd, originalTextFor, ParseException
)
from pyparsing import ZeroOrMore, Word, Optional, Regex, Combine
from pyparsing import Literal as L # noqa
from poetry.semver.version_parser import VersionParser
from .markers import MARKER_EXPR, Marker
LEGACY_REGEX = (
r"""
(?P<operator>(==|!=|<=|>=|<|>))
\s*
(?P<version>
[^,;\s)]* # Since this is a "legacy" specifier, and the version
# string can be just about anything, we match everything
# except for whitespace, a semi-colon for marker support,
# a closing paren since versions can be enclosed in
# them, and a comma since it's a version separator.
)
"""
)
REGEX = (
r"""
(?P<operator>(~=|==|!=|<=|>=|<|>|===))
(?P<version>
(?:
# The identity operators allow for an escape hatch that will
# do an exact string match of the version you wish to install.
# This will not be parsed by PEP 440 and we cannot determine
# any semantic meaning from it. This operator is discouraged
# but included entirely as an escape hatch.
(?<====) # Only match for the identity operator
\s*
[^\s]* # We just match everything, except for whitespace
# since we are only testing for strict identity.
)
|
(?:
# The (non)equality operators allow for wild card and local
# versions to be specified so we have to define these two
# operators separately to enable that.
(?<===|!=) # Only match for equals and not equals
\s*
v?
(?:[0-9]+!)? # epoch
[0-9]+(?:\.[0-9]+)* # release
(?: # pre release
[-_\.]?
(a|b|c|rc|alpha|beta|pre|preview)
[-_\.]?
[0-9]*
)?
(?: # post release
(?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)
)?
# You cannot use a wild card and a dev or local version
# together so group them with a | and make them optional.
(?:
(?:[-_\.]?dev[-_\.]?[0-9]*)? # dev release
(?:\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*)? # local
|
\.\* # Wild card syntax of .*
)?
)
|
(?:
# The compatible operator requires at least two digits in the
# release segment.
(?<=~=) # Only match for the compatible operator
\s*
v?
(?:[0-9]+!)? # epoch
[0-9]+(?:\.[0-9]+)+ # release (We have a + instead of a *)
(?: # pre release
[-_\.]?
(a|b|c|rc|alpha|beta|pre|preview)
[-_\.]?
[0-9]*
)?
(?: # post release
(?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)
)?
(?:[-_\.]?dev[-_\.]?[0-9]*)? # dev release
)
|
(?:
# All other operators only allow a sub set of what the
# (non)equality operators do. Specifically they do not allow
# local versions to be specified nor do they allow the prefix
# matching wild cards.
(?<!==|!=|~=) # We have special cases for these
# operators so we want to make sure they
# don't match here.
\s*
v?
(?:[0-9]+!)? # epoch
[0-9]+(?:\.[0-9]+)* # release
(?: # pre release
[-_\.]?
(a|b|c|rc|alpha|beta|pre|preview)
[-_\.]?
[0-9]*
)?
(?: # post release
(?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)
)?
(?:[-_\.]?dev[-_\.]?[0-9]*)? # dev release
)
)
"""
)
class InvalidRequirement(ValueError):
"""
An invalid requirement was found, users should refer to PEP 508.
"""
ALPHANUM = Word(string.ascii_letters + string.digits)
LBRACKET = L("[").suppress()
RBRACKET = L("]").suppress()
LPAREN = L("(").suppress()
RPAREN = L(")").suppress()
COMMA = L(",").suppress()
SEMICOLON = L(";").suppress()
AT = L("@").suppress()
PUNCTUATION = Word("-_.")
IDENTIFIER_END = ALPHANUM | (ZeroOrMore(PUNCTUATION) + ALPHANUM)
IDENTIFIER = Combine(ALPHANUM + ZeroOrMore(IDENTIFIER_END))
NAME = IDENTIFIER("name")
EXTRA = IDENTIFIER
URI = Regex(r'[^ ]+')("url")
URL = (AT + URI)
EXTRAS_LIST = EXTRA + ZeroOrMore(COMMA + EXTRA)
EXTRAS = (LBRACKET + Optional(EXTRAS_LIST) + RBRACKET)("extras")
VERSION_PEP440 = Regex(REGEX, re.VERBOSE | re.IGNORECASE)
VERSION_LEGACY = Regex(LEGACY_REGEX, re.VERBOSE | re.IGNORECASE)
VERSION_ONE = VERSION_PEP440 ^ VERSION_LEGACY
VERSION_MANY = Combine(VERSION_ONE + ZeroOrMore(COMMA + VERSION_ONE),
joinString=",", adjacent=False)("_raw_spec")
_VERSION_SPEC = Optional(((LPAREN + VERSION_MANY + RPAREN) | VERSION_MANY))
_VERSION_SPEC.setParseAction(lambda s, l, t: t._raw_spec or '')
VERSION_SPEC = originalTextFor(_VERSION_SPEC)("specifier")
VERSION_SPEC.setParseAction(lambda s, l, t: t[1])
MARKER_EXPR = originalTextFor(MARKER_EXPR())("marker")
MARKER_EXPR.setParseAction(
lambda s, l, t: Marker(s[t._original_start:t._original_end])
)
MARKER_SEPERATOR = SEMICOLON
MARKER = MARKER_SEPERATOR + MARKER_EXPR
VERSION_AND_MARKER = VERSION_SPEC + Optional(MARKER)
URL_AND_MARKER = URL + Optional(MARKER)
NAMED_REQUIREMENT = \
NAME + Optional(EXTRAS) + (URL_AND_MARKER | VERSION_AND_MARKER)
REQUIREMENT = stringStart + NAMED_REQUIREMENT + stringEnd
class Requirement(object):
"""Parse a requirement.
Parse a given requirement string into its parts, such as name, specifier,
URL, and extras. Raises InvalidRequirement on a badly-formed requirement
string.
"""
def __init__(self, requirement_string):
try:
req = REQUIREMENT.parseString(requirement_string)
except ParseException as e:
raise InvalidRequirement(
"Invalid requirement, parse error at \"{0!r}\"".format(
requirement_string[e.loc:e.loc + 8]))
self.name = req.name
if req.url:
parsed_url = urlparse.urlparse(req.url)
if not (parsed_url.scheme and parsed_url.netloc) or (
not parsed_url.scheme and not parsed_url.netloc):
raise InvalidRequirement("Invalid URL given")
self.url = req.url
else:
self.url = None
self.extras = set(req.extras.asList() if req.extras else [])
constraint = req.specifier
if not constraint:
constraint = '*'
self.constraint = VersionParser().parse_constraints(constraint)
self.pretty_constraint = constraint
self.marker = req.marker if req.marker else None
def __str__(self):
parts = [self.name]
if self.extras:
parts.append("[{0}]".format(",".join(sorted(self.extras))))
if self.pretty_constraint:
parts.append(self.pretty_constraint)
if self.url:
parts.append("@ {0}".format(self.url))
if self.marker:
parts.append("; {0}".format(self.marker))
return "".join(parts)
def __repr__(self):
return "<Requirement({0!r})>".format(str(self))
...@@ -26,6 +26,7 @@ pip-tools = "^1.11" ...@@ -26,6 +26,7 @@ pip-tools = "^1.11"
requests-toolbelt = "^0.8.0" requests-toolbelt = "^0.8.0"
jsonschema = "^2.6" jsonschema = "^2.6"
pyrsistent = "^0.14.2" pyrsistent = "^0.14.2"
pyparsing = "^2.2"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
pytest = "~3.4" pytest = "~3.4"
......
from poetry.packages import dependency_from_pep_508
def test_dependency_from_pep_508():
name = 'requests'
dep = dependency_from_pep_508(name)
assert dep.name == name
assert str(dep.constraint) == '*'
def test_dependency_from_pep_508_with_version():
name = 'requests==2.18.0'
dep = dependency_from_pep_508(name)
assert dep.name == 'requests'
assert str(dep.constraint) == '== 2.18.0.0'
def test_dependency_from_pep_508_with_parens():
name = 'requests (==2.18.0)'
dep = dependency_from_pep_508(name)
assert dep.name == 'requests'
assert str(dep.constraint) == '== 2.18.0.0'
def test_dependency_from_pep_508_with_constraint():
name = 'requests>=2.12.0,!=2.17.*,<3.0'
dep = dependency_from_pep_508(name)
assert dep.name == 'requests'
assert str(dep.constraint) == '>= 2.12.0.0, != 2.17.*, < 3.0.0.0'
def test_dependency_from_pep_508_with_extras():
name = 'requests==2.18.0; extra == "foo" or extra == "bar"'
dep = dependency_from_pep_508(name)
assert dep.name == 'requests'
assert str(dep.constraint) == '== 2.18.0.0'
assert dep.extras == ['foo', 'bar']
def test_dependency_from_pep_508_with_python_version():
name = (
'requests (==2.18.0); '
'python_version == "2.7" or python_version == "2.6"'
)
dep = dependency_from_pep_508(name)
assert dep.name == 'requests'
assert str(dep.constraint) == '== 2.18.0.0'
assert dep.extras == []
assert dep.python_versions == '~2.7 || ~2.6'
def test_dependency_from_pep_508_with_platform():
name = (
'requests (==2.18.0); '
'sys_platform == "win32" or sys_platform == "darwin"'
)
dep = dependency_from_pep_508(name)
assert dep.name == 'requests'
assert str(dep.constraint) == '== 2.18.0.0'
assert dep.extras == []
assert dep.python_versions == '*'
assert dep.platform == 'win32 || darwin'
def test_dependency_from_pep_508_complex():
name = (
'requests (==2.18.0); '
'python_version >= "2.7" and python_version != "3.2" '
'and (sys_platform == "win32" or sys_platform == "darwin") '
'and extra == "foo"'
)
dep = dependency_from_pep_508(name)
assert dep.name == 'requests'
assert str(dep.constraint) == '== 2.18.0.0'
assert dep.extras == ['foo']
assert dep.python_versions == '>=2.7 !=3.2.*'
assert dep.platform == 'win32 || darwin'
...@@ -38,5 +38,5 @@ def test_package(): ...@@ -38,5 +38,5 @@ def test_package():
win_inet = package.extras['socks'][0] win_inet = package.extras['socks'][0]
assert win_inet.name == 'win-inet-pton' assert win_inet.name == 'win-inet-pton'
assert win_inet.python_versions == '==2.7 || ==2.6' assert win_inet.python_versions == '~2.7 || ~2.6'
assert win_inet.platform == 'win32' assert win_inet.platform == 'win32'
...@@ -57,7 +57,8 @@ def test_parse_constraints_wildcard(parser, input, min, max): ...@@ -57,7 +57,8 @@ def test_parse_constraints_wildcard(parser, input, min, max):
else: else:
expected = max expected = max
assert str(parser.parse_constraints(input)) == str(expected) constraint = parser.parse_constraints(input)
assert str(constraint.constraint) == str(expected)
@pytest.mark.parametrize( @pytest.mark.parametrize(
...@@ -76,7 +77,8 @@ def test_parse_constraints_negative_wildcard(parser, input, min, max): ...@@ -76,7 +77,8 @@ def test_parse_constraints_negative_wildcard(parser, input, min, max):
else: else:
expected = max expected = max
assert str(parser.parse_constraints(input)) == str(expected) constraint = parser.parse_constraints(input)
assert str(constraint.constraint) == str(expected)
@pytest.mark.parametrize( @pytest.mark.parametrize(
......
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