diff --git a/.github/azure-steps.yml b/.github/azure-steps.yml index 18224ba8c..c7722391f 100644 --- a/.github/azure-steps.yml +++ b/.github/azure-steps.yml @@ -27,6 +27,7 @@ steps: - script: python -m mypy spacy displayName: 'Run mypy' + condition: ne(variables['python_version'], '3.10') - task: DeleteFiles@1 inputs: diff --git a/.gitignore b/.gitignore index ac72f2bbf..ac333f958 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ quickstart-training-generator.js cythonize.json spacy/*.html *.cpp +*.c *.so # Vim / VSCode / editors diff --git a/extra/DEVELOPER_DOCS/Satellite Packages.md b/extra/DEVELOPER_DOCS/Satellite Packages.md new file mode 100644 index 000000000..02b06a90e --- /dev/null +++ b/extra/DEVELOPER_DOCS/Satellite Packages.md @@ -0,0 +1,82 @@ +# spaCy Satellite Packages + +This is a list of all the active repos relevant to spaCy besides the main one, with short descriptions, history, and current status. Archived repos will not be covered. + +## Always Included in spaCy + +These packages are always pulled in when you install spaCy. Most of them are direct dependencies, but some are transitive dependencies through other packages. + +- [spacy-legacy](https://github.com/explosion/spacy-legacy): When an architecture in spaCy changes enough to get a new version, the old version is frozen and moved to spacy-legacy. This allows us to keep the core library slim while also preserving backwards compatability. +- [thinc](https://github.com/explosion/thinc): Thinc is the machine learning library that powers trainable components in spaCy. It wraps backends like Numpy, PyTorch, and Tensorflow to provide a functional interface for specifying architectures. +- [catalogue](https://github.com/explosion/catalogue): Small library for adding function registries, like those used for model architectures in spaCy. +- [confection](https://github.com/explosion/confection): This library contains the functionality for config parsing that was formerly contained directly in Thinc. +- [spacy-loggers](https://github.com/explosion/spacy-loggers): Contains loggers beyond the default logger available in spaCy's core code base. This includes loggers integrated with third-party services, which may differ in release cadence from spaCy itself. +- [wasabi](https://github.com/explosion/wasabi): A command line formatting library, used for terminal output in spaCy. +- [srsly](https://github.com/explosion/srsly): A wrapper that vendors several serialization libraries for spaCy. Includes parsers for JSON, JSONL, MessagePack, (extended) Pickle, and YAML. +- [preshed](https://github.com/explosion/preshed): A Cython library for low-level data structures like hash maps, used for memory efficient data storage. +- [cython-blis](https://github.com/explosion/cython-blis): Fast matrix multiplication using BLIS without depending on system libraries. Required by Thinc, rather than spaCy directly. +- [murmurhash](https://github.com/explosion/murmurhash): A wrapper library for a C++ murmurhash implementation, used for string IDs in spaCy and preshed. +- [cymem](https://github.com/explosion/cymem): A small library for RAII-style memory management in Cython. + +## Optional Extensions for spaCy + +These are repos that can be used by spaCy but aren't part of a default installation. Many of these are wrappers to integrate various kinds of third-party libraries. + +- [spacy-transformers](https://github.com/explosion/spacy-transformers): A wrapper for the [HuggingFace Transformers](https://huggingface.co/docs/transformers/index) library, this handles the extensive conversion necessary to coordinate spaCy's powerful `Doc` representation, training pipeline, and the Transformer embeddings. When released, this was known as `spacy-pytorch-transformers`, but it changed to the current name when HuggingFace update the name of their library as well. +- [spacy-huggingface-hub](https://github.com/explosion/spacy-huggingface-hub): This package has a CLI script for uploading a packaged spaCy pipeline (created with `spacy package`) to the [Hugging Face Hub](https://huggingface.co/models). +- [spacy-alignments](https://github.com/explosion/spacy-alignments): A wrapper for the tokenizations library (mentioned below) with a modified build system to simplify cross-platform wheel creation. Used in spacy-transformers for aligning spaCy and HuggingFace tokenizations. +- [spacy-experimental](https://github.com/explosion/spacy-experimental): Experimental components that are not quite ready for inclusion in the main spaCy library. Usually there are unresolved questions around their APIs, so the experimental library allows us to expose them to the community for feedback before fully integrating them. +- [spacy-lookups-data](https://github.com/explosion/spacy-lookups-data): A repository of linguistic data, such as lemmas, that takes up a lot of disk space. Originally created to reduce the size of the spaCy core library. This is mainly useful if you want the data included but aren't using a pretrained pipeline; for the affected languages, the relevant data is included in pretrained pipelines directly. +- [coreferee](https://github.com/explosion/coreferee): Coreference resolution for English, French, German and Polish, optimised for limited training data and easily extensible for further languages. Used as a spaCy pipeline component. +- [spacy-stanza](https://github.com/explosion/spacy-stanza): This is a wrapper that allows the use of Stanford's Stanza library in spaCy. +- [spacy-streamlit](https://github.com/explosion/spacy-streamlit): A wrapper for the Streamlit dashboard building library to help with integrating [displaCy](https://spacy.io/api/top-level/#displacy). +- [spacymoji](https://github.com/explosion/spacymoji): A library to add extra support for emoji to spaCy, such as including character names. +- [thinc-apple-ops](https://github.com/explosion/thinc-apple-ops): A special backend for OSX that uses Apple's native libraries for improved performance. +- [os-signpost](https://github.com/explosion/os-signpost): A Python package that allows you to use the `OSSignposter` API in OSX for performance analysis. +- [spacy-ray](https://github.com/explosion/spacy-ray): A wrapper to integrate spaCy with Ray, a distributed training framework. Currently a work in progress. + +## Prodigy + +[Prodigy](https://prodi.gy) is Explosion's easy to use and highly customizable tool for annotating data. Prodigy itself requires a license, but the repos below contain documentation, examples, and editor or notebook integrations. + +- [prodigy-recipes](https://github.com/explosion/prodigy-recipes): Sample recipes for Prodigy, along with notebooks and other examples of usage. +- [vscode-prodigy](https://github.com/explosion/vscode-prodigy): A VS Code extension that lets you run Prodigy inside VS Code. +- [jupyterlab-prodigy](https://github.com/explosion/jupyterlab-prodigy): An extension for JupyterLab that lets you run Prodigy inside JupyterLab. + +## Independent Tools or Projects + +These are tools that may be related to or use spaCy, but are functional independent projects in their own right as well. + +- [floret](https://github.com/explosion/floret): A modification of fastText to use Bloom Embeddings. Can be used to add vectors with subword features to spaCy, and also works independently in the same manner as fastText. +- [sense2vec](https://github.com/explosion/sense2vec): A library to make embeddings of noun phrases or words coupled with their part of speech. This library uses spaCy. +- [spacy-vectors-builder](https://github.com/explosion/spacy-vectors-builder): This is a spaCy project that builds vectors using floret and a lot of input text. It handles downloading the input data as well as the actual building of vectors. +- [holmes-extractor](https://github.com/explosion/holmes-extractor): Information extraction from English and German texts based on predicate logic. Uses spaCy. +- [healthsea](https://github.com/explosion/healthsea): Healthsea is a project to extract information from comments about health supplements. Structurally, it's a self-contained, large spaCy project. +- [spacy-pkuseg](https://github.com/explosion/spacy-pkuseg): A fork of the pkuseg Chinese tokenizer. Used for Chinese support in spaCy, but also works independently. +- [ml-datasets](https://github.com/explosion/ml-datasets): This repo includes loaders for several standard machine learning datasets, like MNIST or WikiNER, and has historically been used in spaCy example code and documentation. + +## Documentation and Informational Repos + +These repos are used to support the spaCy docs or otherwise present information about spaCy or other Explosion projects. + +- [projects](https://github.com/explosion/projects): The projects repo is used to show detailed examples of spaCy usage. Individual projects can be checked out using the spaCy command line tool, rather than checking out the projects repo directly. +- [spacy-course](https://github.com/explosion/spacy-course): Home to the interactive spaCy course for learning about how to use the library and some basic NLP principles. +- [spacy-io-binder](https://github.com/explosion/spacy-io-binder): Home to the notebooks used for interactive examples in the documentation. + +## Organizational / Meta + +These repos are used for organizing data around spaCy, but are not something an end user would need to install as part of using the library. + +- [spacy-models](https://github.com/explosion/spacy-models): This repo contains metadata (but not training data) for all the spaCy models. This includes information about where their training data came from, version compatability, and performance information. It also includes tests for the model packages, and the built models are hosted as releases of this repo. +- [wheelwright](https://github.com/explosion/wheelwright): A tool for automating our PyPI builds and releases. +- [ec2buildwheel](https://github.com/explosion/ec2buildwheel): A small project that allows you to build Python packages in the manner of cibuildwheel, but on any EC2 image. Used by wheelwright. + +## Other + +Repos that don't fit in any of the above categories. + +- [blis](https://github.com/explosion/blis): A fork of the official BLIS library. The main branch is not updated, but work continues in various branches. This is used for cython-blis. +- [tokenizations](https://github.com/explosion/tokenizations): A library originally by Yohei Tamura to align strings with tolerance to some variations in features like case and diacritics, used for aligning tokens and wordpieces. Adopted and maintained by Explosion, but usually spacy-alignments is used instead. +- [conll-2012](https://github.com/explosion/conll-2012): A repo to hold some slightly cleaned up versions of the official scripts for the CoNLL 2012 shared task involving coreference resolution. Used in the coref project. +- [fastapi-explosion-extras](https://github.com/explosion/fastapi-explosion-extras): Some small tweaks to FastAPI used at Explosion. + diff --git a/licenses/3rd_party_licenses.txt b/licenses/3rd_party_licenses.txt index d58da9c4a..851e09585 100644 --- a/licenses/3rd_party_licenses.txt +++ b/licenses/3rd_party_licenses.txt @@ -127,3 +127,34 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + + +polyleven +--------- + +* Files: spacy/matcher/polyleven.c + +MIT License + +Copyright (c) 2021 Fujimoto Seiji +Copyright (c) 2021 Max Bachmann +Copyright (c) 2022 Nick Mazuk +Copyright (c) 2022 Michael Weiss + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/requirements.txt b/requirements.txt index 3b8d66e0e..3e8501b2f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,4 +34,5 @@ mypy>=0.910,<0.970; platform_machine!='aarch64' types-dataclasses>=0.1.3; python_version < "3.7" types-mock>=0.1.1 types-requests +types-setuptools>=57.0.0 black>=22.0,<23.0 diff --git a/setup.cfg b/setup.cfg index 5fd820a96..2dc5e7042 100644 --- a/setup.cfg +++ b/setup.cfg @@ -76,37 +76,41 @@ transformers = ray = spacy_ray>=0.1.0,<1.0.0 cuda = - cupy>=5.0.0b4,<11.0.0 + cupy>=5.0.0b4,<12.0.0 cuda80 = - cupy-cuda80>=5.0.0b4,<11.0.0 + cupy-cuda80>=5.0.0b4,<12.0.0 cuda90 = - cupy-cuda90>=5.0.0b4,<11.0.0 + cupy-cuda90>=5.0.0b4,<12.0.0 cuda91 = - cupy-cuda91>=5.0.0b4,<11.0.0 + cupy-cuda91>=5.0.0b4,<12.0.0 cuda92 = - cupy-cuda92>=5.0.0b4,<11.0.0 + cupy-cuda92>=5.0.0b4,<12.0.0 cuda100 = - cupy-cuda100>=5.0.0b4,<11.0.0 + cupy-cuda100>=5.0.0b4,<12.0.0 cuda101 = - cupy-cuda101>=5.0.0b4,<11.0.0 + cupy-cuda101>=5.0.0b4,<12.0.0 cuda102 = - cupy-cuda102>=5.0.0b4,<11.0.0 + cupy-cuda102>=5.0.0b4,<12.0.0 cuda110 = - cupy-cuda110>=5.0.0b4,<11.0.0 + cupy-cuda110>=5.0.0b4,<12.0.0 cuda111 = - cupy-cuda111>=5.0.0b4,<11.0.0 + cupy-cuda111>=5.0.0b4,<12.0.0 cuda112 = - cupy-cuda112>=5.0.0b4,<11.0.0 + cupy-cuda112>=5.0.0b4,<12.0.0 cuda113 = - cupy-cuda113>=5.0.0b4,<11.0.0 + cupy-cuda113>=5.0.0b4,<12.0.0 cuda114 = - cupy-cuda114>=5.0.0b4,<11.0.0 + cupy-cuda114>=5.0.0b4,<12.0.0 cuda115 = - cupy-cuda115>=5.0.0b4,<11.0.0 + cupy-cuda115>=5.0.0b4,<12.0.0 cuda116 = - cupy-cuda116>=5.0.0b4,<11.0.0 + cupy-cuda116>=5.0.0b4,<12.0.0 cuda117 = - cupy-cuda117>=5.0.0b4,<11.0.0 + cupy-cuda117>=5.0.0b4,<12.0.0 +cuda11x = + cupy-cuda11x>=11.0.0,<12.0.0 +cuda-autodetect = + cupy-wheel>=11.0.0,<12.0.0 apple = thinc-apple-ops>=0.1.0.dev0,<1.0.0 # Language tokenizers with external dependencies diff --git a/setup.py b/setup.py index ec1bd35fa..c4138aa93 100755 --- a/setup.py +++ b/setup.py @@ -205,6 +205,17 @@ def setup_package(): get_python_inc(plat_specific=True), ] ext_modules = [] + ext_modules.append( + Extension( + "spacy.matcher.levenshtein", + [ + "spacy/matcher/levenshtein.pyx", + "spacy/matcher/polyleven.c", + ], + language="c", + include_dirs=include_dirs, + ) + ) for name in MOD_NAMES: mod_path = name.replace(".", "/") + ".pyx" ext = Extension( diff --git a/spacy/cli/download.py b/spacy/cli/download.py index b7de88729..0c9a32b93 100644 --- a/spacy/cli/download.py +++ b/spacy/cli/download.py @@ -20,7 +20,7 @@ def download_cli( ctx: typer.Context, model: str = Arg(..., help="Name of pipeline package to download"), direct: bool = Opt(False, "--direct", "-d", "-D", help="Force direct download of name + version"), - sdist: bool = Opt(False, "--sdist", "-S", help="Download sdist (.tar.gz) archive instead of pre-built binary wheel") + sdist: bool = Opt(False, "--sdist", "-S", help="Download sdist (.tar.gz) archive instead of pre-built binary wheel"), # fmt: on ): """ @@ -36,7 +36,12 @@ def download_cli( download(model, direct, sdist, *ctx.args) -def download(model: str, direct: bool = False, sdist: bool = False, *pip_args) -> None: +def download( + model: str, + direct: bool = False, + sdist: bool = False, + *pip_args, +) -> None: if ( not (is_package("spacy") or is_package("spacy-nightly")) and "--no-deps" not in pip_args @@ -50,13 +55,10 @@ def download(model: str, direct: bool = False, sdist: bool = False, *pip_args) - "dependencies, you'll have to install them manually." ) pip_args = pip_args + ("--no-deps",) - suffix = SDIST_SUFFIX if sdist else WHEEL_SUFFIX - dl_tpl = "{m}-{v}/{m}-{v}{s}#egg={m}=={v}" if direct: components = model.split("-") model_name = "".join(components[:-1]) version = components[-1] - download_model(dl_tpl.format(m=model_name, v=version, s=suffix), pip_args) else: model_name = model if model in OLD_MODEL_SHORTCUTS: @@ -67,13 +69,26 @@ def download(model: str, direct: bool = False, sdist: bool = False, *pip_args) - model_name = OLD_MODEL_SHORTCUTS[model] compatibility = get_compatibility() version = get_version(model_name, compatibility) - download_model(dl_tpl.format(m=model_name, v=version, s=suffix), pip_args) + + filename = get_model_filename(model_name, version, sdist) + + download_model(filename, pip_args) msg.good( "Download and installation successful", f"You can now load the package via spacy.load('{model_name}')", ) +def get_model_filename(model_name: str, version: str, sdist: bool = False) -> str: + dl_tpl = "{m}-{v}/{m}-{v}{s}" + egg_tpl = "#egg={m}=={v}" + suffix = SDIST_SUFFIX if sdist else WHEEL_SUFFIX + filename = dl_tpl.format(m=model_name, v=version, s=suffix) + if sdist: + filename += egg_tpl.format(m=model_name, v=version) + return filename + + def get_compatibility() -> dict: if is_prerelease_version(about.__version__): version: Optional[str] = about.__version__ @@ -105,6 +120,11 @@ def get_version(model: str, comp: dict) -> str: return comp[model][0] +def get_latest_version(model: str) -> str: + comp = get_compatibility() + return get_version(model, comp) + + def download_model( filename: str, user_pip_args: Optional[Sequence[str]] = None ) -> None: diff --git a/spacy/cli/info.py b/spacy/cli/info.py index e6a1cb616..974bc0f4e 100644 --- a/spacy/cli/info.py +++ b/spacy/cli/info.py @@ -1,10 +1,13 @@ from typing import Optional, Dict, Any, Union, List import platform +import pkg_resources +import json from pathlib import Path from wasabi import Printer, MarkdownRenderer import srsly from ._util import app, Arg, Opt, string_to_list +from .download import get_model_filename, get_latest_version from .. import util from .. import about @@ -16,6 +19,7 @@ def info_cli( markdown: bool = Opt(False, "--markdown", "-md", help="Generate Markdown for GitHub issues"), silent: bool = Opt(False, "--silent", "-s", "-S", help="Don't print anything (just return)"), exclude: str = Opt("labels", "--exclude", "-e", help="Comma-separated keys to exclude from the print-out"), + url: bool = Opt(False, "--url", "-u", help="Print the URL to download the most recent compatible version of the pipeline"), # fmt: on ): """ @@ -23,10 +27,19 @@ def info_cli( print its meta information. Flag --markdown prints details in Markdown for easy copy-pasting to GitHub issues. + Flag --url prints only the download URL of the most recent compatible + version of the pipeline. + DOCS: https://spacy.io/api/cli#info """ exclude = string_to_list(exclude) - info(model, markdown=markdown, silent=silent, exclude=exclude) + info( + model, + markdown=markdown, + silent=silent, + exclude=exclude, + url=url, + ) def info( @@ -35,11 +48,20 @@ def info( markdown: bool = False, silent: bool = True, exclude: Optional[List[str]] = None, + url: bool = False, ) -> Union[str, dict]: msg = Printer(no_print=silent, pretty=not silent) if not exclude: exclude = [] - if model: + if url: + if model is not None: + title = f"Download info for pipeline '{model}'" + data = info_model_url(model) + print(data["download_url"]) + return data + else: + msg.fail("--url option requires a pipeline name", exits=1) + elif model: title = f"Info about pipeline '{model}'" data = info_model(model, silent=silent) else: @@ -99,11 +121,44 @@ def info_model(model: str, *, silent: bool = True) -> Dict[str, Any]: meta["source"] = str(model_path.resolve()) else: meta["source"] = str(model_path) + download_url = info_installed_model_url(model) + if download_url: + meta["download_url"] = download_url return { k: v for k, v in meta.items() if k not in ("accuracy", "performance", "speed") } +def info_installed_model_url(model: str) -> Optional[str]: + """Given a pipeline name, get the download URL if available, otherwise + return None. + + This is only available for pipelines installed as modules that have + dist-info available. + """ + try: + dist = pkg_resources.get_distribution(model) + data = json.loads(dist.get_metadata("direct_url.json")) + return data["url"] + except pkg_resources.DistributionNotFound: + # no such package + return None + except Exception: + # something else, like no file or invalid JSON + return None + + +def info_model_url(model: str) -> Dict[str, Any]: + """Return the download URL for the latest version of a pipeline.""" + version = get_latest_version(model) + + filename = get_model_filename(model, version) + download_url = about.__download_url__ + "/" + filename + release_tpl = "https://github.com/explosion/spacy-models/releases/tag/{m}-{v}" + release_url = release_tpl.format(m=model, v=version) + return {"download_url": download_url, "release_url": release_url} + + def get_markdown( data: Dict[str, Any], title: Optional[str] = None, diff --git a/spacy/cli/project/run.py b/spacy/cli/project/run.py index 734803bc4..d42d95465 100644 --- a/spacy/cli/project/run.py +++ b/spacy/cli/project/run.py @@ -195,6 +195,8 @@ def validate_subcommand( msg.fail(f"No commands or workflows defined in {PROJECT_FILE}", exits=1) if subcommand not in commands and subcommand not in workflows: help_msg = [] + if subcommand in ["assets", "asset"]: + help_msg.append("Did you mean to run: python -m spacy project assets?") if commands: help_msg.append(f"Available commands: {', '.join(commands)}") if workflows: diff --git a/spacy/cli/templates/quickstart_training.jinja b/spacy/cli/templates/quickstart_training.jinja index ae11dcafc..58864883a 100644 --- a/spacy/cli/templates/quickstart_training.jinja +++ b/spacy/cli/templates/quickstart_training.jinja @@ -271,13 +271,8 @@ factory = "tok2vec" [components.tok2vec.model.embed] @architectures = "spacy.MultiHashEmbed.v2" width = ${components.tok2vec.model.encode.width} -{% if has_letters -%} attrs = ["NORM", "PREFIX", "SUFFIX", "SHAPE"] -rows = [5000, 2500, 2500, 2500] -{% else -%} -attrs = ["ORTH", "SHAPE"] -rows = [5000, 2500] -{% endif -%} +rows = [5000, 1000, 2500, 2500] include_static_vectors = {{ "true" if optimize == "accuracy" else "false" }} [components.tok2vec.model.encode] diff --git a/spacy/cli/templates/quickstart_training_recommendations.yml b/spacy/cli/templates/quickstart_training_recommendations.yml index a7bf9b74a..27945e27a 100644 --- a/spacy/cli/templates/quickstart_training_recommendations.yml +++ b/spacy/cli/templates/quickstart_training_recommendations.yml @@ -271,4 +271,3 @@ zh: accuracy: name: bert-base-chinese size_factor: 3 - has_letters: false diff --git a/spacy/errors.py b/spacy/errors.py index 608305a06..f55b378e9 100644 --- a/spacy/errors.py +++ b/spacy/errors.py @@ -230,8 +230,9 @@ class Errors(metaclass=ErrorsWithCodes): "initialized component.") E004 = ("Can't set up pipeline component: a factory for '{name}' already " "exists. Existing factory: {func}. New factory: {new_func}") - E005 = ("Pipeline component '{name}' returned None. If you're using a " - "custom component, maybe you forgot to return the processed Doc?") + E005 = ("Pipeline component '{name}' returned {returned_type} instead of a " + "Doc. If you're using a custom component, maybe you forgot to " + "return the processed Doc?") E006 = ("Invalid constraints for adding pipeline component. You can only " "set one of the following: before (component name or index), " "after (component name or index), first (True) or last (True). " @@ -706,7 +707,7 @@ class Errors(metaclass=ErrorsWithCodes): "need to modify the pipeline, use the built-in methods like " "`nlp.add_pipe`, `nlp.remove_pipe`, `nlp.disable_pipe` or " "`nlp.enable_pipe` instead.") - E927 = ("Can't write to frozen list Maybe you're trying to modify a computed " + E927 = ("Can't write to frozen list. Maybe you're trying to modify a computed " "property or default function argument?") E928 = ("A KnowledgeBase can only be serialized to/from from a directory, " "but the provided argument {loc} points to a file.") diff --git a/spacy/lang/la/__init__.py b/spacy/lang/la/__init__.py index 5f2cccee3..15b87c5b9 100644 --- a/spacy/lang/la/__init__.py +++ b/spacy/lang/la/__init__.py @@ -6,7 +6,7 @@ from .lex_attrs import LEX_ATTRS class LatinDefaults(BaseDefaults): tokenizer_exceptions = TOKENIZER_EXCEPTIONS - stop_words = STOP_WORDS + stop_words = STOP_WORDS lex_attr_getters = LEX_ATTRS diff --git a/spacy/lang/la/lex_attrs.py b/spacy/lang/la/lex_attrs.py index 9348a811a..9efb4dd3c 100644 --- a/spacy/lang/la/lex_attrs.py +++ b/spacy/lang/la/lex_attrs.py @@ -2,7 +2,9 @@ from ...attrs import LIKE_NUM import re # cf. Goyvaerts/Levithan 2009; case-insensitive, allow 4 -roman_numerals_compile = re.compile(r'(?i)^(?=[MDCLXVI])M*(C[MD]|D?C{0,4})(X[CL]|L?X{0,4})(I[XV]|V?I{0,4})$') +roman_numerals_compile = re.compile( + r"(?i)^(?=[MDCLXVI])M*(C[MD]|D?C{0,4})(X[CL]|L?X{0,4})(I[XV]|V?I{0,4})$" +) _num_words = set( """ diff --git a/spacy/lang/la/tokenizer_exceptions.py b/spacy/lang/la/tokenizer_exceptions.py index 905304188..060f6e085 100644 --- a/spacy/lang/la/tokenizer_exceptions.py +++ b/spacy/lang/la/tokenizer_exceptions.py @@ -9,21 +9,67 @@ _exc = { "tecum": [{ORTH: "te"}, {ORTH: "cum"}], "nobiscum": [{ORTH: "nobis"}, {ORTH: "cum"}], "vobiscum": [{ORTH: "vobis"}, {ORTH: "cum"}], - "uobiscum": [{ORTH: "uobis"}, {ORTH: "cum"}], + "uobiscum": [{ORTH: "uobis"}, {ORTH: "cum"}], } for orth in [ - - 'A.', 'Agr.', 'Ap.', 'C.', 'Cn.', 'D.', 'F.', 'K.', 'L.', "M'.", 'M.', 'Mam.', 'N.', 'Oct.', - 'Opet.', 'P.', 'Paul.', 'Post.', 'Pro.', 'Q.', 'S.', 'Ser.', 'Sert.', 'Sex.', 'St.', 'Sta.', - 'T.', 'Ti.', 'V.', 'Vol.', 'Vop.', 'U.', 'Uol.', 'Uop.', - - 'Ian.', 'Febr.', 'Mart.', 'Apr.', 'Mai.', 'Iun.', 'Iul.', 'Aug.', 'Sept.', 'Oct.', 'Nov.', 'Nou.', - 'Dec.', - - 'Non.', 'Id.', 'A.D.', - - 'Coll.', 'Cos.', 'Ord.', 'Pl.', 'S.C.', 'Suff.', 'Trib.', + "A.", + "Agr.", + "Ap.", + "C.", + "Cn.", + "D.", + "F.", + "K.", + "L.", + "M'.", + "M.", + "Mam.", + "N.", + "Oct.", + "Opet.", + "P.", + "Paul.", + "Post.", + "Pro.", + "Q.", + "S.", + "Ser.", + "Sert.", + "Sex.", + "St.", + "Sta.", + "T.", + "Ti.", + "V.", + "Vol.", + "Vop.", + "U.", + "Uol.", + "Uop.", + "Ian.", + "Febr.", + "Mart.", + "Apr.", + "Mai.", + "Iun.", + "Iul.", + "Aug.", + "Sept.", + "Oct.", + "Nov.", + "Nou.", + "Dec.", + "Non.", + "Id.", + "A.D.", + "Coll.", + "Cos.", + "Ord.", + "Pl.", + "S.C.", + "Suff.", + "Trib.", ]: _exc[orth] = [{ORTH: orth}] diff --git a/spacy/language.py b/spacy/language.py index ec330753c..34a06e576 100644 --- a/spacy/language.py +++ b/spacy/language.py @@ -1028,8 +1028,8 @@ class Language: raise ValueError(Errors.E109.format(name=name)) from e except Exception as e: error_handler(name, proc, [doc], e) - if doc is None: - raise ValueError(Errors.E005.format(name=name)) + if not isinstance(doc, Doc): + raise ValueError(Errors.E005.format(name=name, returned_type=type(doc))) return doc def disable_pipes(self, *names) -> "DisabledPipes": diff --git a/spacy/matcher/__init__.py b/spacy/matcher/__init__.py index 286844787..a4f164847 100644 --- a/spacy/matcher/__init__.py +++ b/spacy/matcher/__init__.py @@ -1,5 +1,6 @@ from .matcher import Matcher from .phrasematcher import PhraseMatcher from .dependencymatcher import DependencyMatcher +from .levenshtein import levenshtein -__all__ = ["Matcher", "PhraseMatcher", "DependencyMatcher"] +__all__ = ["Matcher", "PhraseMatcher", "DependencyMatcher", "levenshtein"] diff --git a/spacy/matcher/levenshtein.pyx b/spacy/matcher/levenshtein.pyx new file mode 100644 index 000000000..8463d913d --- /dev/null +++ b/spacy/matcher/levenshtein.pyx @@ -0,0 +1,15 @@ +# cython: profile=True, binding=True, infer_types=True +from cpython.object cimport PyObject +from libc.stdint cimport int64_t + +from typing import Optional + + +cdef extern from "polyleven.c": + int64_t polyleven(PyObject *o1, PyObject *o2, int64_t k) + + +cpdef int64_t levenshtein(a: str, b: str, k: Optional[int] = None): + if k is None: + k = -1 + return polyleven(a, b, k) diff --git a/spacy/matcher/matcher.pyx b/spacy/matcher/matcher.pyx index 5105f69ed..e1dba01a2 100644 --- a/spacy/matcher/matcher.pyx +++ b/spacy/matcher/matcher.pyx @@ -1,5 +1,5 @@ # cython: infer_types=True, cython: profile=True -from typing import List +from typing import List, Iterable from libcpp.vector cimport vector from libc.stdint cimport int32_t, int8_t @@ -867,20 +867,27 @@ class _SetPredicate: def __call__(self, Token token): if self.is_extension: - value = get_string_id(token._.get(self.attr)) + value = token._.get(self.attr) else: value = get_token_attr_for_matcher(token.c, self.attr) - if self.predicate in ("IS_SUBSET", "IS_SUPERSET", "INTERSECTS"): + if self.predicate in ("IN", "NOT_IN"): + if isinstance(value, (str, int)): + value = get_string_id(value) + else: + return False + elif self.predicate in ("IS_SUBSET", "IS_SUPERSET", "INTERSECTS"): + # ensure that all values are enclosed in a set if self.attr == MORPH: # break up MORPH into individual Feat=Val values value = set(get_string_id(v) for v in MorphAnalysis.from_id(self.vocab, value)) + elif isinstance(value, (str, int)): + value = set((get_string_id(value),)) + elif isinstance(value, Iterable) and all(isinstance(v, (str, int)) for v in value): + value = set(get_string_id(v) for v in value) else: - # treat a single value as a list - if isinstance(value, (str, int)): - value = set([get_string_id(value)]) - else: - value = set(get_string_id(v) for v in value) + return False + if self.predicate == "IN": return value in self.value elif self.predicate == "NOT_IN": diff --git a/spacy/matcher/polyleven.c b/spacy/matcher/polyleven.c new file mode 100644 index 000000000..2f2b8826c --- /dev/null +++ b/spacy/matcher/polyleven.c @@ -0,0 +1,384 @@ +/* + * Adapted from Polyleven (https://ceptord.net/) + * + * Source: https://github.com/fujimotos/polyleven/blob/c3f95a080626c5652f0151a2e449963288ccae84/polyleven.c + * + * Copyright (c) 2021 Fujimoto Seiji + * Copyright (c) 2021 Max Bachmann + * Copyright (c) 2022 Nick Mazuk + * Copyright (c) 2022 Michael Weiss + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include +#include + +#define MIN(a,b) ((a) < (b) ? (a) : (b)) +#define MAX(a,b) ((a) > (b) ? (a) : (b)) +#define CDIV(a,b) ((a) / (b) + ((a) % (b) > 0)) +#define BIT(i,n) (((i) >> (n)) & 1) +#define FLIP(i,n) ((i) ^ ((uint64_t) 1 << (n))) +#define ISASCII(kd) ((kd) == PyUnicode_1BYTE_KIND) + +/* + * Bare bone of PyUnicode + */ +struct strbuf { + void *ptr; + int kind; + int64_t len; +}; + +static void strbuf_init(struct strbuf *s, PyObject *o) +{ + s->ptr = PyUnicode_DATA(o); + s->kind = PyUnicode_KIND(o); + s->len = PyUnicode_GET_LENGTH(o); +} + +#define strbuf_read(s, i) PyUnicode_READ((s)->kind, (s)->ptr, (i)) + +/* + * An encoded mbleven model table. + * + * Each 8-bit integer represents an edit sequence, with using two + * bits for a single operation. + * + * 01 = DELETE, 10 = INSERT, 11 = REPLACE + * + * For example, 13 is '1101' in binary notation, so it means + * DELETE + REPLACE. + */ +static const uint8_t MBLEVEN_MATRIX[] = { + 3, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, + 15, 9, 6, 0, 0, 0, 0, 0, + 13, 7, 0, 0, 0, 0, 0, 0, + 5, 0, 0, 0, 0, 0, 0, 0, + 63, 39, 45, 57, 54, 30, 27, 0, + 61, 55, 31, 37, 25, 22, 0, 0, + 53, 29, 23, 0, 0, 0, 0, 0, + 21, 0, 0, 0, 0, 0, 0, 0, +}; + +#define MBLEVEN_MATRIX_GET(k, d) ((((k) + (k) * (k)) / 2 - 1) + (d)) * 8 + +static int64_t mbleven_ascii(char *s1, int64_t len1, + char *s2, int64_t len2, int k) +{ + int pos; + uint8_t m; + int64_t i, j, c, r; + + pos = MBLEVEN_MATRIX_GET(k, len1 - len2); + r = k + 1; + + while (MBLEVEN_MATRIX[pos]) { + m = MBLEVEN_MATRIX[pos++]; + i = j = c = 0; + while (i < len1 && j < len2) { + if (s1[i] != s2[j]) { + c++; + if (!m) break; + if (m & 1) i++; + if (m & 2) j++; + m >>= 2; + } else { + i++; + j++; + } + } + c += (len1 - i) + (len2 - j); + r = MIN(r, c); + if (r < 2) { + return r; + } + } + return r; +} + +static int64_t mbleven(PyObject *o1, PyObject *o2, int64_t k) +{ + int pos; + uint8_t m; + int64_t i, j, c, r; + struct strbuf s1, s2; + + strbuf_init(&s1, o1); + strbuf_init(&s2, o2); + + if (s1.len < s2.len) + return mbleven(o2, o1, k); + + if (k > 3) + return -1; + + if (k < s1.len - s2.len) + return k + 1; + + if (ISASCII(s1.kind) && ISASCII(s2.kind)) + return mbleven_ascii(s1.ptr, s1.len, s2.ptr, s2.len, k); + + pos = MBLEVEN_MATRIX_GET(k, s1.len - s2.len); + r = k + 1; + + while (MBLEVEN_MATRIX[pos]) { + m = MBLEVEN_MATRIX[pos++]; + i = j = c = 0; + while (i < s1.len && j < s2.len) { + if (strbuf_read(&s1, i) != strbuf_read(&s2, j)) { + c++; + if (!m) break; + if (m & 1) i++; + if (m & 2) j++; + m >>= 2; + } else { + i++; + j++; + } + } + c += (s1.len - i) + (s2.len - j); + r = MIN(r, c); + if (r < 2) { + return r; + } + } + return r; +} + +/* + * Data structure to store Peq (equality bit-vector). + */ +struct blockmap_entry { + uint32_t key[128]; + uint64_t val[128]; +}; + +struct blockmap { + int64_t nr; + struct blockmap_entry *list; +}; + +#define blockmap_key(c) ((c) | 0x80000000U) +#define blockmap_hash(c) ((c) % 128) + +static int blockmap_init(struct blockmap *map, struct strbuf *s) +{ + int64_t i; + struct blockmap_entry *be; + uint32_t c, k; + uint8_t h; + + map->nr = CDIV(s->len, 64); + map->list = calloc(1, map->nr * sizeof(struct blockmap_entry)); + if (map->list == NULL) { + PyErr_NoMemory(); + return -1; + } + + for (i = 0; i < s->len; i++) { + be = &(map->list[i / 64]); + c = strbuf_read(s, i); + h = blockmap_hash(c); + k = blockmap_key(c); + + while (be->key[h] && be->key[h] != k) + h = blockmap_hash(h + 1); + be->key[h] = k; + be->val[h] |= (uint64_t) 1 << (i % 64); + } + return 0; +} + +static void blockmap_clear(struct blockmap *map) +{ + if (map->list) + free(map->list); + map->list = NULL; + map->nr = 0; +} + +static uint64_t blockmap_get(struct blockmap *map, int block, uint32_t c) +{ + struct blockmap_entry *be; + uint8_t h; + uint32_t k; + + h = blockmap_hash(c); + k = blockmap_key(c); + + be = &(map->list[block]); + while (be->key[h] && be->key[h] != k) + h = blockmap_hash(h + 1); + return be->key[h] == k ? be->val[h] : 0; +} + +/* + * Myers' bit-parallel algorithm + * + * See: G. Myers. "A fast bit-vector algorithm for approximate string + * matching based on dynamic programming." Journal of the ACM, 1999. + */ +static int64_t myers1999_block(struct strbuf *s1, struct strbuf *s2, + struct blockmap *map) +{ + uint64_t Eq, Xv, Xh, Ph, Mh, Pv, Mv, Last; + uint64_t *Mhc, *Phc; + int64_t i, b, hsize, vsize, Score; + uint8_t Pb, Mb; + + hsize = CDIV(s1->len, 64); + vsize = CDIV(s2->len, 64); + Score = s2->len; + + Phc = malloc(hsize * 2 * sizeof(uint64_t)); + if (Phc == NULL) { + PyErr_NoMemory(); + return -1; + } + Mhc = Phc + hsize; + memset(Phc, -1, hsize * sizeof(uint64_t)); + memset(Mhc, 0, hsize * sizeof(uint64_t)); + Last = (uint64_t)1 << ((s2->len - 1) % 64); + + for (b = 0; b < vsize; b++) { + Mv = 0; + Pv = (uint64_t) -1; + Score = s2->len; + + for (i = 0; i < s1->len; i++) { + Eq = blockmap_get(map, b, strbuf_read(s1, i)); + + Pb = BIT(Phc[i / 64], i % 64); + Mb = BIT(Mhc[i / 64], i % 64); + + Xv = Eq | Mv; + Xh = ((((Eq | Mb) & Pv) + Pv) ^ Pv) | Eq | Mb; + + Ph = Mv | ~ (Xh | Pv); + Mh = Pv & Xh; + + if (Ph & Last) Score++; + if (Mh & Last) Score--; + + if ((Ph >> 63) ^ Pb) + Phc[i / 64] = FLIP(Phc[i / 64], i % 64); + + if ((Mh >> 63) ^ Mb) + Mhc[i / 64] = FLIP(Mhc[i / 64], i % 64); + + Ph = (Ph << 1) | Pb; + Mh = (Mh << 1) | Mb; + + Pv = Mh | ~ (Xv | Ph); + Mv = Ph & Xv; + } + } + free(Phc); + return Score; +} + +static int64_t myers1999_simple(uint8_t *s1, int64_t len1, uint8_t *s2, int64_t len2) +{ + uint64_t Peq[256]; + uint64_t Eq, Xv, Xh, Ph, Mh, Pv, Mv, Last; + int64_t i; + int64_t Score = len2; + + memset(Peq, 0, sizeof(Peq)); + + for (i = 0; i < len2; i++) + Peq[s2[i]] |= (uint64_t) 1 << i; + + Mv = 0; + Pv = (uint64_t) -1; + Last = (uint64_t) 1 << (len2 - 1); + + for (i = 0; i < len1; i++) { + Eq = Peq[s1[i]]; + + Xv = Eq | Mv; + Xh = (((Eq & Pv) + Pv) ^ Pv) | Eq; + + Ph = Mv | ~ (Xh | Pv); + Mh = Pv & Xh; + + if (Ph & Last) Score++; + if (Mh & Last) Score--; + + Ph = (Ph << 1) | 1; + Mh = (Mh << 1); + + Pv = Mh | ~ (Xv | Ph); + Mv = Ph & Xv; + } + return Score; +} + +static int64_t myers1999(PyObject *o1, PyObject *o2) +{ + struct strbuf s1, s2; + struct blockmap map; + int64_t ret; + + strbuf_init(&s1, o1); + strbuf_init(&s2, o2); + + if (s1.len < s2.len) + return myers1999(o2, o1); + + if (ISASCII(s1.kind) && ISASCII(s2.kind) && s2.len < 65) + return myers1999_simple(s1.ptr, s1.len, s2.ptr, s2.len); + + if (blockmap_init(&map, &s2)) + return -1; + + ret = myers1999_block(&s1, &s2, &map); + blockmap_clear(&map); + return ret; +} + +/* + * Interface functions + */ +static int64_t polyleven(PyObject *o1, PyObject *o2, int64_t k) +{ + int64_t len1, len2; + + len1 = PyUnicode_GET_LENGTH(o1); + len2 = PyUnicode_GET_LENGTH(o2); + + if (len1 < len2) + return polyleven(o2, o1, k); + + if (k == 0) + return PyUnicode_Compare(o1, o2) ? 1 : 0; + + if (0 < k && k < len1 - len2) + return k + 1; + + if (len2 == 0) + return len1; + + if (0 < k && k < 4) + return mbleven(o1, o2, k); + + return myers1999(o1, o2); +} diff --git a/spacy/ml/callbacks.py b/spacy/ml/callbacks.py index 18290b947..3b60ec2ab 100644 --- a/spacy/ml/callbacks.py +++ b/spacy/ml/callbacks.py @@ -89,11 +89,14 @@ def pipes_with_nvtx_range( types.MethodType(nvtx_range_wrapper_for_pipe_method, pipe), func ) - # Try to preserve the original function signature. + # We need to preserve the original function signature so that + # the original parameters are passed to pydantic for validation downstream. try: wrapped_func.__signature__ = inspect.signature(func) # type: ignore except: - pass + # Can fail for Cython methods that do not have bindings. + warnings.warn(Warnings.W122.format(method=name, pipe=pipe.name)) + continue try: setattr( diff --git a/spacy/pipeline/pipe.pyx b/spacy/pipeline/pipe.pyx index 4e3ae1cf0..8407acc45 100644 --- a/spacy/pipeline/pipe.pyx +++ b/spacy/pipeline/pipe.pyx @@ -1,4 +1,4 @@ -# cython: infer_types=True, profile=True +# cython: infer_types=True, profile=True, binding=True from typing import Optional, Tuple, Iterable, Iterator, Callable, Union, Dict import srsly import warnings diff --git a/spacy/pipeline/trainable_pipe.pyx b/spacy/pipeline/trainable_pipe.pyx index 76b0733cf..3f0507d4b 100644 --- a/spacy/pipeline/trainable_pipe.pyx +++ b/spacy/pipeline/trainable_pipe.pyx @@ -1,4 +1,4 @@ -# cython: infer_types=True, profile=True +# cython: infer_types=True, profile=True, binding=True from typing import Iterable, Iterator, Optional, Dict, Tuple, Callable import srsly from thinc.api import set_dropout_rate, Model, Optimizer diff --git a/spacy/tests/conftest.py b/spacy/tests/conftest.py index 0395ba7ca..742bfcc6a 100644 --- a/spacy/tests/conftest.py +++ b/spacy/tests/conftest.py @@ -258,7 +258,7 @@ def ko_tokenizer_tokenizer(): @pytest.fixture(scope="module") def la_tokenizer(): - return get_lang_class("la")().tokenizer + return get_lang_class("la")().tokenizer @pytest.fixture(scope="session") diff --git a/spacy/tests/lang/la/test_exception.py b/spacy/tests/lang/la/test_exception.py index 04bc1d489..966ae22cf 100644 --- a/spacy/tests/lang/la/test_exception.py +++ b/spacy/tests/lang/la/test_exception.py @@ -1,5 +1,6 @@ import pytest + def test_la_tokenizer_handles_exc_in_text(la_tokenizer): text = "scio te omnia facturum, ut nobiscum quam primum sis" tokens = la_tokenizer(text) diff --git a/spacy/tests/lang/la/test_text.py b/spacy/tests/lang/la/test_text.py index 11676b92b..48e7359a4 100644 --- a/spacy/tests/lang/la/test_text.py +++ b/spacy/tests/lang/la/test_text.py @@ -1,6 +1,7 @@ import pytest from spacy.lang.la.lex_attrs import like_num + @pytest.mark.parametrize( "text,match", [ @@ -13,7 +14,7 @@ from spacy.lang.la.lex_attrs import like_num ("ix", True), ("MMXXII", True), ("0", True), - ("1", True), + ("1", True), ("quattuor", True), ("decem", True), ("tertius", True), @@ -27,6 +28,7 @@ def test_lex_attrs_like_number(la_tokenizer, text, match): assert len(tokens) == 1 assert tokens[0].like_num == match + @pytest.mark.parametrize("word", ["quinque"]) def test_la_lex_attrs_capitals(word): assert like_num(word) diff --git a/spacy/tests/matcher/test_levenshtein.py b/spacy/tests/matcher/test_levenshtein.py new file mode 100644 index 000000000..6c7793f63 --- /dev/null +++ b/spacy/tests/matcher/test_levenshtein.py @@ -0,0 +1,36 @@ +import pytest +from spacy.matcher import levenshtein + + +# empty string plus 10 random ASCII, 10 random unicode, and 2 random long tests +# from polyleven +@pytest.mark.parametrize( + "dist,a,b", + [ + (0, "", ""), + (4, "bbcb", "caba"), + (3, "abcb", "cacc"), + (3, "aa", "ccc"), + (1, "cca", "ccac"), + (1, "aba", "aa"), + (4, "bcbb", "abac"), + (3, "acbc", "bba"), + (3, "cbba", "a"), + (2, "bcc", "ba"), + (4, "aaa", "ccbb"), + (3, "うあい", "いいうい"), + (2, "あううい", "うあい"), + (3, "いういい", "うううあ"), + (2, "うい", "あいあ"), + (2, "いあい", "いう"), + (1, "いい", "あいい"), + (3, "あうあ", "いいああ"), + (4, "いあうう", "ううああ"), + (3, "いあいい", "ういああ"), + (3, "いいああ", "ううあう"), + (166,"TCTGGGCACGGATTCGTCAGATTCCATGTCCATATTTGAGGCTCTTGCAGGCAAAATTTGGGCATGTGAACTCCTTATAGTCCCCGTGC","ATATGGATTGGGGGCATTCAAAGATACGGTTTCCCTTTCTTCAGTTTCGCGCGGCGCACGTCCGGGTGCGAGCCAGTTCGTCTTACTCACATTGTCGACTTCACGAATCGCGCATGATGTGCTTAGCCTGTACTTACGAACGAACTTTCGGTCCAAATACATTCTATCAACACCGAGGTATCCGTGCCACACGCCGAAGCTCGACCGTGTTCGTTGAGAGGTGGAAATGGTAAAAGATGAACATAGTC"), + (111,"GGTTCGGCCGAATTCATAGAGCGTGGTAGTCGACGGTATCCCGCCTGGTAGGGGCCCCTTCTACCTAGCGGAAGTTTGTCAGTACTCTATAACACGAGGGCCTCTCACACCCTAGATCGTCCAGCCACTCGAAGATCGCAGCACCCTTACAGAAAGGCATTAATGTTTCTCCTAGCACTTGTGCAATGGTGAAGGAGTGATG","CGTAACACTTCGCGCTACTGGGCTGCAACGTCTTGGGCATACATGCAAGATTATCTAATGCAAGCTTGAGCCCCGCTTGCGGAATTTCCCTAATCGGGGTCCCTTCCTGTTACGATAAGGACGCGTGCACT"), + ], +) +def test_levenshtein(dist, a, b): + assert levenshtein(a, b) == dist diff --git a/spacy/tests/matcher/test_matcher_api.py b/spacy/tests/matcher/test_matcher_api.py index 7c16da9f8..ac905eeb4 100644 --- a/spacy/tests/matcher/test_matcher_api.py +++ b/spacy/tests/matcher/test_matcher_api.py @@ -368,6 +368,16 @@ def test_matcher_intersect_value_operator(en_vocab): doc[0]._.ext = ["A", "B"] assert len(matcher(doc)) == 1 + # INTERSECTS matches nothing for iterables that aren't all str or int + matcher = Matcher(en_vocab) + pattern = [{"_": {"ext": {"INTERSECTS": ["Abx", "C"]}}}] + matcher.add("M", [pattern]) + doc = Doc(en_vocab, words=["a", "b", "c"]) + doc[0]._.ext = [["Abx"], "B"] + assert len(matcher(doc)) == 0 + doc[0]._.ext = ["Abx", "B"] + assert len(matcher(doc)) == 1 + # INTERSECTS with an empty pattern list matches nothing matcher = Matcher(en_vocab) pattern = [{"_": {"ext": {"INTERSECTS": []}}}] @@ -476,14 +486,22 @@ def test_matcher_extension_set_membership(en_vocab): assert len(matches) == 0 -@pytest.mark.xfail(reason="IN predicate must handle sequence values in extensions") def test_matcher_extension_in_set_predicate(en_vocab): matcher = Matcher(en_vocab) Token.set_extension("ext", default=[]) pattern = [{"_": {"ext": {"IN": ["A", "C"]}}}] matcher.add("M", [pattern]) doc = Doc(en_vocab, words=["a", "b", "c"]) + + # The IN predicate expects an exact match between the + # extension value and one of the pattern's values. doc[0]._.ext = ["A", "B"] + assert len(matcher(doc)) == 0 + + doc[0]._.ext = ["A"] + assert len(matcher(doc)) == 0 + + doc[0]._.ext = "A" assert len(matcher(doc)) == 1 diff --git a/spacy/tests/package/test_requirements.py b/spacy/tests/package/test_requirements.py index e20227455..b403f274f 100644 --- a/spacy/tests/package/test_requirements.py +++ b/spacy/tests/package/test_requirements.py @@ -17,6 +17,7 @@ def test_build_dependencies(): "types-dataclasses", "types-mock", "types-requests", + "types-setuptools", ] # ignore language-specific packages that shouldn't be installed by all libs_ignore_setup = [ diff --git a/spacy/tests/test_language.py b/spacy/tests/test_language.py index 6f3ba8acc..03a98d32f 100644 --- a/spacy/tests/test_language.py +++ b/spacy/tests/test_language.py @@ -670,3 +670,25 @@ def test_dot_in_factory_names(nlp): with pytest.raises(ValueError, match="not permitted"): Language.factory("my.evil.component.v1", func=evil_component) + + +def test_component_return(): + """Test that an error is raised if components return a type other than a + doc.""" + nlp = English() + + @Language.component("test_component_good_pipe") + def good_pipe(doc): + return doc + + nlp.add_pipe("test_component_good_pipe") + nlp("text") + nlp.remove_pipe("test_component_good_pipe") + + @Language.component("test_component_bad_pipe") + def bad_pipe(doc): + return doc.text + + nlp.add_pipe("test_component_bad_pipe") + with pytest.raises(ValueError, match="instead of a Doc"): + nlp("text") diff --git a/website/docs/api/cli.md b/website/docs/api/cli.md index cbd1f794a..e5cd3089b 100644 --- a/website/docs/api/cli.md +++ b/website/docs/api/cli.md @@ -77,14 +77,15 @@ $ python -m spacy info [--markdown] [--silent] [--exclude] $ python -m spacy info [model] [--markdown] [--silent] [--exclude] ``` -| Name | Description | -| ------------------------------------------------ | --------------------------------------------------------------------------------------------- | -| `model` | A trained pipeline, i.e. package name or path (optional). ~~Optional[str] \(option)~~ | -| `--markdown`, `-md` | Print information as Markdown. ~~bool (flag)~~ | -| `--silent`, `-s` 2.0.12 | Don't print anything, just return the values. ~~bool (flag)~~ | -| `--exclude`, `-e` | Comma-separated keys to exclude from the print-out. Defaults to `"labels"`. ~~Optional[str]~~ | -| `--help`, `-h` | Show help message and available arguments. ~~bool (flag)~~ | -| **PRINTS** | Information about your spaCy installation. | +| Name | Description | +| ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- | +| `model` | A trained pipeline, i.e. package name or path (optional). ~~Optional[str] \(option)~~ | +| `--markdown`, `-md` | Print information as Markdown. ~~bool (flag)~~ | +| `--silent`, `-s` 2.0.12 | Don't print anything, just return the values. ~~bool (flag)~~ | +| `--exclude`, `-e` | Comma-separated keys to exclude from the print-out. Defaults to `"labels"`. ~~Optional[str]~~ | +| `--url`, `-u` 3.5.0 | Print the URL to download the most recent compatible version of the pipeline. Requires a pipeline name. ~~bool (flag)~~ | +| `--help`, `-h` | Show help message and available arguments. ~~bool (flag)~~ | +| **PRINTS** | Information about your spaCy installation. | ## validate {#validate new="2" tag="command"} diff --git a/website/docs/api/example.md b/website/docs/api/example.md index ca9d3c056..63768d58f 100644 --- a/website/docs/api/example.md +++ b/website/docs/api/example.md @@ -23,11 +23,13 @@ both documents. > ```python > from spacy.tokens import Doc > from spacy.training import Example -> -> words = ["hello", "world", "!"] -> spaces = [True, False, False] -> predicted = Doc(nlp.vocab, words=words, spaces=spaces) -> reference = parse_gold_doc(my_data) +> pred_words = ["Apply", "some", "sunscreen"] +> pred_spaces = [True, True, False] +> gold_words = ["Apply", "some", "sun", "screen"] +> gold_spaces = [True, True, False, False] +> gold_tags = ["VERB", "DET", "NOUN", "NOUN"] +> predicted = Doc(nlp.vocab, words=pred_words, spaces=pred_spaces) +> reference = Doc(nlp.vocab, words=gold_words, spaces=gold_spaces, tags=gold_tags) > example = Example(predicted, reference) > ``` @@ -286,10 +288,14 @@ Calculate alignment tables between two tokenizations. ### Alignment attributes {#alignment-attributes"} -| Name | Description | -| ----- | --------------------------------------------------------------------- | -| `x2y` | The `Ragged` object holding the alignment from `x` to `y`. ~~Ragged~~ | -| `y2x` | The `Ragged` object holding the alignment from `y` to `x`. ~~Ragged~~ | +Alignment attributes are managed using `AlignmentArray`, which is a +simplified version of Thinc's [Ragged](https://thinc.ai/docs/api-types#ragged) +type that only supports the `data` and `length` attributes. + +| Name | Description | +| ----- | ------------------------------------------------------------------------------------- | +| `x2y` | The `AlignmentArray` object holding the alignment from `x` to `y`. ~~AlignmentArray~~ | +| `y2x` | The `AlignmentArray` object holding the alignment from `y` to `x`. ~~AlignmentArray~~ | @@ -309,10 +315,10 @@ tokenizations add up to the same string. For example, you'll be able to align > spacy_tokens = ["obama", "'s", "podcast"] > alignment = Alignment.from_strings(bert_tokens, spacy_tokens) > a2b = alignment.x2y -> assert list(a2b.dataXd) == [0, 1, 1, 2] +> assert list(a2b.data) == [0, 1, 1, 2] > ``` > -> If `a2b.dataXd[1] == a2b.dataXd[2] == 1`, that means that `A[1]` (`"'"`) and +> If `a2b.data[1] == a2b.data[2] == 1`, that means that `A[1]` (`"'"`) and > `A[2]` (`"s"`) both align to `B[1]` (`"'s"`). ### Alignment.from_strings {#classmethod tag="function"} diff --git a/website/docs/api/top-level.md b/website/docs/api/top-level.md index 220b2d6e9..bc53fc868 100644 --- a/website/docs/api/top-level.md +++ b/website/docs/api/top-level.md @@ -887,6 +887,27 @@ backprop passes. | `backprop_color` | Color identifier for backpropagation passes. Defaults to `-1`. ~~int~~ | | **CREATES** | A function that takes the current `nlp` and wraps forward/backprop passes in NVTX ranges. ~~Callable[[Language], Language]~~ | +### spacy.models_and_pipes_with_nvtx_range.v1 {#models_and_pipes_with_nvtx_range tag="registered function" new="3.4"} + +> #### Example config +> +> ```ini +> [nlp] +> after_pipeline_creation = {"@callbacks":"spacy.models_and_pipes_with_nvtx_range.v1"} +> ``` + +Recursively wrap both the models and methods of each pipe using +[NVTX](https://nvidia.github.io/NVTX/) range markers. By default, the following +methods are wrapped: `pipe`, `predict`, `set_annotations`, `update`, `rehearse`, +`get_loss`, `initialize`, `begin_update`, `finish_update`, `update`. + +| Name | Description | +| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `forward_color` | Color identifier for model forward passes. Defaults to `-1`. ~~int~~ | +| `backprop_color` | Color identifier for model backpropagation passes. Defaults to `-1`. ~~int~~ | +| `additional_pipe_functions` | Additional pipeline methods to wrap. Keys are pipeline names and values are lists of method identifiers. Defaults to `None`. ~~Optional[Dict[str, List[str]]]~~ | +| **CREATES** | A function that takes the current `nlp` and wraps pipe models and methods in NVTX ranges. ~~Callable[[Language], Language]~~ | + ## Training data and alignment {#gold source="spacy/training"} ### training.offsets_to_biluo_tags {#offsets_to_biluo_tags tag="function"} diff --git a/website/docs/usage/linguistic-features.md b/website/docs/usage/linguistic-features.md index 82472c67e..099678c40 100644 --- a/website/docs/usage/linguistic-features.md +++ b/website/docs/usage/linguistic-features.md @@ -1422,9 +1422,9 @@ other_tokens = ["i", "listened", "to", "obama", "'", "s", "podcasts", "."] spacy_tokens = ["i", "listened", "to", "obama", "'s", "podcasts", "."] align = Alignment.from_strings(other_tokens, spacy_tokens) print(f"a -> b, lengths: {align.x2y.lengths}") # array([1, 1, 1, 1, 1, 1, 1, 1]) -print(f"a -> b, mapping: {align.x2y.dataXd}") # array([0, 1, 2, 3, 4, 4, 5, 6]) : two tokens both refer to "'s" +print(f"a -> b, mapping: {align.x2y.data}") # array([0, 1, 2, 3, 4, 4, 5, 6]) : two tokens both refer to "'s" print(f"b -> a, lengths: {align.y2x.lengths}") # array([1, 1, 1, 1, 2, 1, 1]) : the token "'s" refers to two tokens -print(f"b -> a, mappings: {align.y2x.dataXd}") # array([0, 1, 2, 3, 4, 5, 6, 7]) +print(f"b -> a, mappings: {align.y2x.data}") # array([0, 1, 2, 3, 4, 5, 6, 7]) ``` Here are some insights from the alignment information generated in the example @@ -1433,10 +1433,10 @@ above: - The one-to-one mappings for the first four tokens are identical, which means they map to each other. This makes sense because they're also identical in the input: `"i"`, `"listened"`, `"to"` and `"obama"`. -- The value of `x2y.dataXd[6]` is `5`, which means that `other_tokens[6]` +- The value of `x2y.data[6]` is `5`, which means that `other_tokens[6]` (`"podcasts"`) aligns to `spacy_tokens[5]` (also `"podcasts"`). -- `x2y.dataXd[4]` and `x2y.dataXd[5]` are both `4`, which means that both tokens - 4 and 5 of `other_tokens` (`"'"` and `"s"`) align to token 4 of `spacy_tokens` +- `x2y.data[4]` and `x2y.data[5]` are both `4`, which means that both tokens 4 + and 5 of `other_tokens` (`"'"` and `"s"`) align to token 4 of `spacy_tokens` (`"'s"`). diff --git a/website/docs/usage/models.md b/website/docs/usage/models.md index 56992e7e3..6971ac8b4 100644 --- a/website/docs/usage/models.md +++ b/website/docs/usage/models.md @@ -365,15 +365,32 @@ pipeline package can be found. To download a trained pipeline directly using [pip](https://pypi.python.org/pypi/pip), point `pip install` to the URL or local path of the wheel file or archive. Installing the wheel is usually more -efficient. To find the direct link to a package, head over to the -[releases](https://github.com/explosion/spacy-models/releases), right click on -the archive link and copy it to your clipboard. +efficient. + +> #### Pipeline Package URLs {#pipeline-urls} +> +> Pretrained pipeline distributions are hosted on +> [Github Releases](https://github.com/explosion/spacy-models/releases), and you +> can find download links there, as well as on the model page. You can also get +> URLs directly from the command line by using `spacy info` with the `--url` +> flag, which may be useful for automation. +> +> ```bash +> spacy info en_core_web_sm --url +> ``` +> +> This command will print the URL for the latest version of a pipeline +> compatible with the version of spaCy you're using. Note that in order to look +> up the compatibility information an internet connection is required. ```bash # With external URL $ pip install https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.0.0/en_core_web_sm-3.0.0-py3-none-any.whl $ pip install https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.0.0/en_core_web_sm-3.0.0.tar.gz +# Using spacy info to get the external URL +$ pip install $(spacy info en_core_web_sm --url) + # With local file $ pip install /Users/you/en_core_web_sm-3.0.0-py3-none-any.whl $ pip install /Users/you/en_core_web_sm-3.0.0.tar.gz @@ -514,21 +531,16 @@ should be specifying them directly. Because pipeline packages are valid Python packages, you can add them to your application's `requirements.txt`. If you're running your own internal PyPi installation, you can upload the pipeline packages there. pip's -[requirements file format](https://pip.pypa.io/en/latest/reference/pip_install/#requirements-file-format) -supports both package names to download via a PyPi server, as well as direct -URLs. +[requirements file format](https://pip.pypa.io/en/latest/reference/requirements-file-format/) +supports both package names to download via a PyPi server, as well as +[direct URLs](#pipeline-urls). ```text ### requirements.txt spacy>=3.0.0,<4.0.0 -https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.0.0/en_core_web_sm-3.0.0.tar.gz#egg=en_core_web_sm +en_core_web_sm @ https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.4.0/en_core_web_sm-3.4.0-py3-none-any.whl ``` -Specifying `#egg=` with the package name tells pip which package to expect from -the download URL. This way, the package won't be re-downloaded and overwritten -if it's already installed - just like when you're downloading a package from -PyPi. - All pipeline packages are versioned and specify their spaCy dependency. This ensures cross-compatibility and lets you specify exact version requirements for each pipeline. If you've [trained](/usage/training) your own pipeline, you can diff --git a/website/docs/usage/projects.md b/website/docs/usage/projects.md index 566ae561b..35150035a 100644 --- a/website/docs/usage/projects.md +++ b/website/docs/usage/projects.md @@ -758,7 +758,7 @@ and [`dvc repro`](https://dvc.org/doc/command-reference/repro) to reproduce the workflow or individual commands. ```cli -$ python -m spacy project dvc [workflow_name] +$ python -m spacy project dvc [project_dir] [workflow_name] ``` diff --git a/website/meta/universe.json b/website/meta/universe.json index 6c8caa6a6..9145855c6 100644 --- a/website/meta/universe.json +++ b/website/meta/universe.json @@ -1192,7 +1192,7 @@ "slogan": "Fast, flexible and transparent sentiment analysis", "description": "Asent is a rule-based sentiment analysis library for Python made using spaCy. It is inspired by VADER, but uses a more modular ruleset, that allows the user to change e.g. the method for finding negations. Furthermore it includes visualisers to visualize the model predictions, making the model easily interpretable.", "github": "kennethenevoldsen/asent", - "pip": "aseny", + "pip": "asent", "code_example": [ "import spacy", "import asent", diff --git a/website/src/templates/models.js b/website/src/templates/models.js index df53f8c3c..16a2360d5 100644 --- a/website/src/templates/models.js +++ b/website/src/templates/models.js @@ -76,6 +76,7 @@ const MODEL_META = { benchmark_ner: 'NER accuracy', benchmark_speed: 'Speed', compat: 'Latest compatible package version for your spaCy installation', + download_link: 'Download link for the pipeline', } const LABEL_SCHEME_META = { @@ -138,6 +139,13 @@ function formatAccuracy(data, lang) { .filter(item => item) } +function formatDownloadLink(lang, name, version) { + const fullName = `${lang}_${name}-${version}` + const filename = `${fullName}-py3-none-any.whl` + const url = `https://github.com/explosion/spacy-models/releases/download/${fullName}/${filename}` + return {filename} +} + function formatModelMeta(data) { return { fullName: `${data.lang}_${data.name}-${data.version}`, @@ -154,6 +162,7 @@ function formatModelMeta(data) { labels: isEmptyObj(data.labels) ? null : data.labels, vectors: formatVectors(data.vectors), accuracy: formatAccuracy(data.performance, data.lang), + download_link: formatDownloadLink(data.lang, data.name, data.version), } } @@ -244,6 +253,7 @@ const Model = ({ { label: 'Components', content: components, help: MODEL_META.components }, { label: 'Pipeline', content: pipeline, help: MODEL_META.pipeline }, { label: 'Vectors', content: meta.vectors, help: MODEL_META.vecs }, + { label: 'Download Link', content: meta.download_link, help: MODEL_META.download_link }, { label: 'Sources', content: sources, help: MODEL_META.sources }, { label: 'Author', content: author }, { label: 'License', content: license }, diff --git a/website/src/widgets/quickstart-install.js b/website/src/widgets/quickstart-install.js index 61c0678dd..0d2186acb 100644 --- a/website/src/widgets/quickstart-install.js +++ b/website/src/widgets/quickstart-install.js @@ -9,7 +9,7 @@ const DEFAULT_PLATFORM = 'x86' const DEFAULT_MODELS = ['en'] const DEFAULT_OPT = 'efficiency' const DEFAULT_HARDWARE = 'cpu' -const DEFAULT_CUDA = 'cuda113' +const DEFAULT_CUDA = 'cuda-autodetect' const CUDA = { '8.0': 'cuda80', '9.0': 'cuda90', @@ -17,15 +17,7 @@ const CUDA = { '9.2': 'cuda92', '10.0': 'cuda100', '10.1': 'cuda101', - '10.2': 'cuda102', - '11.0': 'cuda110', - '11.1': 'cuda111', - '11.2': 'cuda112', - '11.3': 'cuda113', - '11.4': 'cuda114', - '11.5': 'cuda115', - '11.6': 'cuda116', - '11.7': 'cuda117', + '10.2, 11.0+': 'cuda-autodetect', } const LANG_EXTRAS = ['ja'] // only for languages with models