Use more sophisticated version parsing logic

This commit is contained in:
Ines Montani 2020-05-30 15:01:58 +02:00
parent bed62991ad
commit e47e5a4b10
6 changed files with 41 additions and 43 deletions

View File

@ -5,7 +5,7 @@ import sys
from wasabi import msg from wasabi import msg
from .. import about from .. import about
from ..util import is_package from ..util import is_package, get_base_version
def download( def download(
@ -63,8 +63,7 @@ def get_json(url, desc):
def get_compatibility(): def get_compatibility():
version = about.__version__ version = get_base_version(about.__version__)
version = version.rsplit(".dev", 1)[0]
comp_table = get_json(about.__compatibility__, "compatibility table") comp_table = get_json(about.__compatibility__, "compatibility table")
comp = comp_table["spacy"] comp = comp_table["spacy"]
if version not in comp: if version not in comp:
@ -73,7 +72,7 @@ def get_compatibility():
def get_version(model, comp): def get_version(model, comp):
model = model.rsplit(".dev", 1)[0] model = get_base_version(model)
if model not in comp: if model not in comp:
msg.fail( msg.fail(
f"No compatible model found for '{model}' (spaCy v{about.__version__})", f"No compatible model found for '{model}' (spaCy v{about.__version__})",

View File

@ -90,7 +90,7 @@ def generate_meta(model_path, existing_meta, msg):
("license", "License", meta.get("license", "MIT")), ("license", "License", meta.get("license", "MIT")),
] ]
nlp = util.load_model_from_path(Path(model_path)) nlp = util.load_model_from_path(Path(model_path))
meta["spacy_version"] = about.__version__ meta["spacy_version"] = util.get_model_version_range(about.__version__)
meta["pipeline"] = nlp.pipe_names meta["pipeline"] = nlp.pipe_names
meta["vectors"] = { meta["vectors"] = {
"width": nlp.vocab.vectors_length, "width": nlp.vocab.vectors_length,
@ -138,7 +138,7 @@ def list_files(data_dir):
def list_requirements(meta): def list_requirements(meta):
parent_package = meta.get('parent_package', 'spacy') parent_package = meta.get('parent_package', 'spacy')
requirements = [parent_package + '>=' + meta['spacy_version']] requirements = [parent_package + meta['spacy_version']]
if 'setup_requires' in meta: if 'setup_requires' in meta:
requirements += meta['setup_requires'] requirements += meta['setup_requires']
if 'requirements' in meta: if 'requirements' in meta:

View File

@ -4,7 +4,7 @@ import requests
from wasabi import msg from wasabi import msg
from .. import about from .. import about
from ..util import get_package_version, get_installed_models, split_version from ..util import get_package_version, get_installed_models, get_base_version
from ..util import get_package_path, get_model_meta, is_compatible_model from ..util import get_package_path, get_model_meta, is_compatible_model
@ -14,7 +14,7 @@ def validate():
with the installed models. Should be run after `pip install -U spacy`. with the installed models. Should be run after `pip install -U spacy`.
""" """
model_pkgs, compat = get_model_pkgs() model_pkgs, compat = get_model_pkgs()
spacy_version = about.__version__.rsplit(".dev", 1)[0] spacy_version = get_base_version(about.__version__)
current_compat = compat.get(spacy_version, {}) current_compat = compat.get(spacy_version, {})
if not current_compat: if not current_compat:
msg.warn(f"No compatible models found for v{spacy_version} of spaCy") msg.warn(f"No compatible models found for v{spacy_version} of spaCy")
@ -78,13 +78,12 @@ def get_model_pkgs():
version = get_package_version(pkg_name) version = get_package_version(pkg_name)
if package in compat: if package in compat:
is_compat = version in compat[package] is_compat = version in compat[package]
v_maj, v_min = split_version(about.__version__) spacy_version = about.__version__
spacy_version = f"{v_maj}.{v_min}"
else: else:
model_path = get_package_path(package) model_path = get_package_path(package)
model_meta = get_model_meta(model_path) model_meta = get_model_meta(model_path)
is_compat = is_compatible_model(model_meta)
spacy_version = model_meta.get("spacy_version", "n/a") spacy_version = model_meta.get("spacy_version", "n/a")
is_compat = is_compatible_model(spacy_version)
pkgs[pkg_name] = { pkgs[pkg_name] = {
"name": package, "name": package,
"version": version, "version": version,

View File

@ -191,13 +191,14 @@ class Language(object):
@property @property
def meta(self): def meta(self):
spacy_version = util.get_model_version_range(about.__version__)
if self.vocab.lang: if self.vocab.lang:
self._meta.setdefault("lang", self.vocab.lang) self._meta.setdefault("lang", self.vocab.lang)
else: else:
self._meta.setdefault("lang", self.lang) self._meta.setdefault("lang", self.lang)
self._meta.setdefault("name", "model") self._meta.setdefault("name", "model")
self._meta.setdefault("version", "0.0.0") self._meta.setdefault("version", "0.0.0")
self._meta.setdefault("spacy_version", about.__version__) self._meta.setdefault("spacy_version", spacy_version)
self._meta.setdefault("description", "") self._meta.setdefault("description", "")
self._meta.setdefault("author", "") self._meta.setdefault("author", "")
self._meta.setdefault("email", "") self._meta.setdefault("email", "")

View File

@ -95,7 +95,15 @@ def test_ascii_filenames():
@pytest.mark.parametrize( @pytest.mark.parametrize(
"version,compatible", "version,compatible",
[(spacy_version, True), ("2.0.0", False), (">=1.2.3,<4.5.6", False)], [
(spacy_version, True),
(f">={spacy_version}", True),
("2.0.0", False),
(">=2.0.0", True),
(">=1.0.0,<2.1.1", False),
(">=1.2.3,<4.5.6", True),
("n/a", None),
],
) )
def test_is_compatible_model(version, compatible): def test_is_compatible_model(version, compatible):
assert util.is_compatible_model({"spacy_version": version}) is compatible assert util.is_compatible_model(version) is compatible

View File

@ -14,6 +14,8 @@ import srsly
import catalogue import catalogue
import sys import sys
import warnings import warnings
from packaging.specifiers import SpecifierSet, InvalidSpecifier
from packaging.version import Version, InvalidVersion
try: try:
@ -236,42 +238,31 @@ def get_package_version(name):
return None return None
def split_version(version): def is_compatible_model(constraint):
"""RETURNS (tuple): Two integers, the major and minor spaCy version.""" version = Version(about.__version__)
pieces = version.split(".", 3) if constraint[0].isdigit():
return int(pieces[0]), int(pieces[1]) # Handle cases where exact version is provided as constraint
constraint = f"=={constraint}"
try:
def is_compatible_model(meta): spec = SpecifierSet(constraint)
"""Check if a model is compatible with the current version of spaCy, based except InvalidSpecifier:
on its meta.json. We compare the version of spaCy the model was created with
with the current version. If the minor version is different, it's considered
incompatible.
meta (dict): The model's meta.
RETURNS (bool / None): Whether the model is compatible with the current
spaCy or None if we don't have enough info.
"""
cur_v = about.__version__
pkg_v = meta.get("spacy_version")
if not pkg_v or not isinstance(pkg_v, str):
return None return None
# Handle spacy_version values like >=x,<y, just in case # Allow prereleases and dev versions
pkg_v = re.sub(r"[^0-9.]", "", pkg_v.split(",")[0]) spec.prereleases = True
cur_major, cur_minor = split_version(cur_v) return version in spec
pkg_major, pkg_minor = split_version(pkg_v)
if cur_major != pkg_major or cur_minor != pkg_minor:
return False
return True
def get_model_version_range(version): def get_model_version_range(spacy_version):
"""Generate a version range like >=1.2.3,<1.3.0 based on a given spaCy """Generate a version range like >=1.2.3,<1.3.0 based on a given spaCy
version. Models are always compatible across patch versions but not version. Models are always compatible across patch versions but not
across minor or major versions. across minor or major versions.
""" """
major, minor = split_version(version) release = Version(spacy_version).release
return f">={version},<{major}.{minor + 1}.0" return f">={spacy_version},<{release[0]}.{release[1] + 1}.0"
def get_base_version(version):
return Version(version).base_version
def load_config(path, create_objects=False): def load_config(path, create_objects=False):