diff --git a/spacy/errors.py b/spacy/errors.py index ce931f0a1..94a0218a7 100644 --- a/spacy/errors.py +++ b/spacy/errors.py @@ -113,6 +113,12 @@ class Warnings(object): "ignored during training.") # TODO: fix numbering after merging develop into master + W094 = ("Model '{model}' ({model_version}) specifies an under-constrained " + "spaCy version requirement: {version}. This can lead to compatibility " + "problems with older versions, or as new spaCy versions are " + "released, because the model may say it's compatible when it's " + 'not. Consider changing the "spacy_version" in your meta.json to a ' + "version range, with a lower and upper pin. For example: {example}") W095 = ("Model '{model}' ({model_version}) requires spaCy {version} and is " "incompatible with the current version ({current}). This may lead " "to unexpected results or runtime errors. To resolve this, " diff --git a/spacy/tests/test_misc.py b/spacy/tests/test_misc.py index e4b4e570c..4e6c0e652 100644 --- a/spacy/tests/test_misc.py +++ b/spacy/tests/test_misc.py @@ -109,3 +109,21 @@ def test_ascii_filenames(): ) def test_is_compatible_version(version, constraint, compatible): assert util.is_compatible_version(version, constraint) is compatible + + +@pytest.mark.parametrize( + "constraint,expected", + [ + ("3.0.0", False), + ("==3.0.0", False), + (">=2.3.0", True), + (">2.0.0", True), + ("<=2.0.0", True), + (">2.0.0,<3.0.0", False), + (">=2.0.0,<3.0.0", False), + ("!=1.1,>=1.0,~=1.0", True), + ("n/a", None), + ], +) +def test_is_unconstrained_version(constraint, expected): + assert util.is_unconstrained_version(constraint) is expected diff --git a/spacy/util.py b/spacy/util.py index 97cc5a8d7..bc6c98a82 100644 --- a/spacy/util.py +++ b/spacy/util.py @@ -264,6 +264,31 @@ def is_compatible_version(version, constraint, prereleases=True): return version in spec +def is_unconstrained_version(constraint, prereleases=True): + # We have an exact version, this is the ultimate constrained version + if constraint[0].isdigit(): + return False + try: + spec = SpecifierSet(constraint) + except InvalidSpecifier: + return None + spec.prereleases = prereleases + specs = [sp for sp in spec] + # We only have one version spec and it defines > or >= + if len(specs) == 1 and specs[0].operator in (">", ">="): + return True + # One specifier is exact version + if any(sp.operator in ("==") for sp in specs): + return False + has_upper = any(sp.operator in ("<", "<=") for sp in specs) + has_lower = any(sp.operator in (">", ">=") for sp in specs) + # We have a version spec that defines an upper and lower bound + if has_upper and has_lower: + return False + # Everything else, like only an upper version, only a lower version etc. + return True + + def get_model_version_range(spacy_version): """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 @@ -334,14 +359,21 @@ def get_model_meta(path): raise ValueError(Errors.E054.format(setting=setting)) if "spacy_version" in meta: if not is_compatible_version(about.__version__, meta["spacy_version"]): - warnings.warn( - Warnings.W095.format( - model=f"{meta['lang']}_{meta['name']}", - model_version=meta["version"], - version=meta["spacy_version"], - current=about.__version__, - ) + warn_msg = Warnings.W095.format( + model=f"{meta['lang']}_{meta['name']}", + model_version=meta["version"], + version=meta["spacy_version"], + current=about.__version__, ) + warnings.warn(warn_msg) + if is_unconstrained_version(meta["spacy_version"]): + warn_msg = Warnings.W094.format( + model=f"{meta['lang']}_{meta['name']}", + model_version=meta["version"], + version=meta["spacy_version"], + example=get_model_version_range(about.__version__), + ) + warnings.warn(warn_msg) return meta