Package: grammar-detector
Grammar-DetectorA tool for detecting grammatical features in sentences, clauses, and phrases in just a few lines of code. This tool is one piece of a larger project to facilitate the creation of reading exercises for language instruction. It is designed to determine if a text contains sentences relevant to the desired grammatical feature. Any languagesupported by spaCyis theoretically supported.The patterns for these grammatical features are defined in YAML files calledpatternsetsin lieu of writing code. These YAML files expand the capabilities of theGrammarDetector. The input text to be analyzed is compared against the patterns in thepatternsets. In other words, writing more code is unnecessary for supporting new grammatical features. This means that inaccurate results arise from inaccurate patterns (and not from the code itself). To mitigate errors, unittests can be defined in thepatternsets.For the purposes of this tool, a sentence is roughly defined as:An independent clause with sentence-final punctuation and additional clauses, orA dependent clause with sentence-final punctuation which may satisfy the concept of a 'complete thought' in the context of surrounding sentences (e.g. "We tried updating it. Which didn't work. Nor did the reinstall.")OverviewThe core of this tool is theGrammarDetector. After construction, it can be used in two different ways:Using theGrammarDetector.__call__(self, input: str)instance method on the input to runautomatically.Looping through theGrammarDetector.detectors: list[Detector]instance property and using theDetector.__call__(self, input: str)instance method on the input to runmanually.DependenciesDependencies:python(>=3.9)                                                        -- frequent use of f-strings and type hintspyyaml-- loading patternset YAML filesspacy-- rule-based grammatical pattern matchingspacy-lookups-data-- spaCy dependencytabulate-- printing token tables to write patternsDev dependencies:black-- opinionated code formattermypy-- type checkingpython-lsp-server-- IDE integrationtypes-pyyaml-- type checkingtypes-tabulate-- type checkingtypes-setuptools-- type checkingDetector FeaturesCurrently supports the ability to:Evaluate a sentence, clause, or phrase for its grammatical featuresProduce results that are reader-friendly and reader-usefulUse built-in grammatical features with just 3 lines of code (import, construct, and call)Create your own grammatical feature by passing the filepath of a simplepatternsetYAML file with spaCyTokensConvert input into a table ofTokensto aid in visualizing and conceptualizingpatternsConvert input into list ofTokenlikesto aid in creating and improvingpatternsDefine and run tests inpatternsetsto evaluate the accuracy of the patternsFragment an input into noun chunks automatically before theDetectoris runFuture features:Add support for validatingpatternsetYAML files (currently validatespatternsonly)Publish the built-inpatternsetYAML files as a separate packageGrammatical FeaturesAll current patterns are relatively naive, so they do not yet effectively handle recursivity. This problem can be solved by 1) writing recursive patterns or 2) writing alternative patterns and suffixing therulenameproperty with numbers (e.g. ditransitive-1 and ditransitive-2).Determiners:IndefiniteDefiniteOtherNonePersons:1st2nd3rdTense-Aspects:Present simplePresent simple passivePast simplePast simple passiveFuture simple willFuture simple will passiveFuture simple be-going-toFuture simple be-going-to passivePresent continuousPresent continuous passivePast continuousPast continuous passiveFuture continuousFuture continuous passivePresent perfectPresent perfect passivePast perfectPast perfect passiveFuture perfectFuture perfect passivePresent perfect continuousPresent perfect continuous passivePast perfect continuousPast perfect continuous passiveFuture perfect continuousFuture perfect continuous passiveTransitivity and Valency:Impersonal (valency == 0)Intransitive (valency == 1)Transitive (valency == 2)Ditransitive (valency == 3)Voices:ActivePassiveInstallationThe default language model,en_core_web_md(40 MB), can be substituted with anotherspaCy language model, such asen_core_web_lg(560 MB) oren_core_web_sm(12 MB). Be sure to disablebuiltins(see below) if using a model from a language other than English.$ pip install grammar-detector
$ python -m spacy download en_core_web_mdUsageUsage: 0) Constructing the GrammarDetector# my_script.py

from grammardetector import GrammarDetector


# Default values
settings = {  
    "builtins": True,
    "language_model": "en_core_web_md",
    "patternset_path": "",  # Custom patternsets
    "verbose": False,
    "very_verbose": False,
}
grammar_detector = GrammarDetector(**settings)  # Optionally, pass in **settingsUsage: 1) Running the GrammarDetector# my_script.py

from grammardetector import GrammarDetector


grammar_detector = GrammarDetector()
input: str = "The dog was chasing a cat into the house."
results = grammar_detector(input)Usage: 2) Interpreting the Results# my_script.py

from grammardetector import GrammarDetector, Match
from typing import Union


ResultsType = dict[str, Union[str, list[Match]]]


grammar_detector = GrammarDetector()
input: str = "The dog was chasing a cat into the house."
results: ResultsType = grammar_detector(input)

print(results)
# {
#     'input': 'The dog was chasing a cat into the house.', 
#     'voices': [<active: was chasing>], 
#     'tense_aspects': [<past continuous: was chasing>], 
#     'persons': [<3rd: dog>, <3rd: cat>, <3rd: house>], 
#     'determiners': [<definite: The dog>, <indefinite: a cat>, <definite: the house>], 
#     'transitivity': [<ditransitive: dog was chasing a cat into the house>]
# }

feature: str = "tense_aspects"
verb_tense: Match = results[feature][0]

print(verb_tense)
# <past continuous: was chasing>

print(verb_tense.rulename)
# "past continuous"

print(verb_tense.span)
# "was chasing"

print(verb_tense.span_features)
# {
#     'span': was chasing, 
#     'phrase': 'was chasing', 
#     'root': 'chasing', 
#     'root_head': 'chasing', 
#     'pos': 'VERB', 
#     'tag': 'VBG', 
#     'dep': 'ROOT', 
#     'phrase_lemma': 'be chase', 
#     'root_lemma': 'chase', 
#     'pos_desc': 'verb', 
#     'tag_desc': 'verb, gerund or present participle', 
#     'dep_desc': 'root'
# }Usage: Loading Patterns in Custom Patternset YAML Filesfrom grammardetector import GrammarDetector


grammar_detector = GrammarDetector(
    builtins=False,
    patternset_path="path/to/my/patternset/files/",
)
input: str = "The dog was chasing a cat into the house."
results = grammar_detector(input)

print(results)
# Prints only your custom featuresUsage: Running Tests in Custom Patternset YAML Files# my_script.py

from grammardetector import GrammarDetector


grammar_detector = GrammarDetector(patternset_path="path/to/my/patternset/files/")
grammar_detector.run_tests()

# Run the tests for the built-in patternsets
grammar_detector.run_tests(builtin_tests=True)Usage: Printing Token Tables# my_script.py

from grammardetector import GrammarDetector


grammar_detector = GrammarDetector()
input: str = "The dog was chasing a cat into the house."

default_kwargs = {
    "pos": True,
    "tag": True,
    "dependency": True,
    "lemma": True,
}
table: str = grammar_detector.token_table(input, **default_kwargs)
print(table)WordPOSPOS DefinitionTagTag DefinitionDep.Dep. DefinitionLemma.TheDETdeterminerDTdeterminerdetdeterminerthedogNOUNnounNNnoun, singular or massnsubjnominal subjectdogwasAUXauxiliaryVBDverb, past tenseauxauxiliarybechasingVERBverbVBGverb, gerund or present participleROOTrootchaseaDETdeterminerDTdeterminerdetdetermineracatNOUNnounNNnoun, singular or massdobjdirect objectcatintoADPadpositionINconjunction, subordinating or prepositionprepprepositional modifierintotheDETdeterminerDTdeterminerdetdeterminerthehouseNOUNnounNNnoun, singular or masspobjobject of prepositionhouse.PUNCTpunctuation.punctuation mark, sentence closerpunctpunctuation.Usage: Printing Token Data# my_script.py

from grammardetector import GrammarDetector, Tokenlike


# TokenlikeKeys = Literal["pos", "tag", "dep", "lemma", "word"]
# Tokenlike = dict[TokenlikeKeys, str]

grammar_detector = GrammarDetector()
input: str = "The dog was chasing a cat into the house."

default_kwargs = {
    "pos": True,
    "tag": True,
    "dependency": True,
    "lemma": False,
    "word": False,
}
data: list[Tokenlike] = grammar_detector.token_data(input, **default_kwargs)
for entry in data:
    print(entry)

# {'pos': 'DET', 'tag': 'DT', 'dep': 'det'}
# {'pos': 'NOUN', 'tag': 'NN', 'dep': 'nsubj'}
# {'pos': 'AUX', 'tag': 'VBD', 'dep': 'aux'}
# {'pos': 'VERB', 'tag': 'VBG', 'dep': 'ROOT'}
# {'pos': 'DET', 'tag': 'DT', 'dep': 'det'}
# {'pos': 'NOUN', 'tag': 'NN', 'dep': 'dobj'}
# {'pos': 'ADP', 'tag': 'IN', 'dep': 'prep'}
# {'pos': 'DET', 'tag': 'DT', 'dep': 'det'}
# {'pos': 'NOUN', 'tag': 'NN', 'dep': 'pobj'}
# {'pos': 'PUNCT', 'tag': '.', 'dep': 'punct'}Usage: Troubleshooting# my_script.py

from grammardetector import GrammarDetector


grammar_detector = GrammarDetector(verbose=True, very_verbose=False)  # very_verbose prioritized over verbose
# Prints logs for configuring and loading patternsets

input: str = "The dog was chasing a cat into the house."
results = grammar_detector(input)
# Prints logs for running the matcher and interpreting the resultsUnder the HoodThis section describes the components used to build and runDetectorsinside theGrammarDetector. To expand on the built-in features of theGrammarDetector, understanding howpatternsetYAML files are created, configured, and loaded is critical. To load your ownpatternsetfiles, pass the file or directory path to thepatternset_pathkeyword argument when constructing theGrammarDetector.The GrammarDetector classTheGrammarDetectorclass is the entrypoint for loading inpatternsetfiles and evaluating text input. By running theGrammarDetector.__call__(self, input: str)instance method, the text input will be compared against both the built-inpatternsetsand the providedpatternsetsvia thepatternset_pathkeyword argument. TheDetectorRepositoryis contained under the hood, which in turn contains theDetectors. Extracting the internalDetectorsfrom theGrammarDetectoris unnecessary but easy via theGrammarDetector.detectors: list[Detector]instance property.# my_script.py

from grammardetector import Detector, GrammarDetector


grammar_detector = GrammarDetector(patternset_path="path/to/my/patternset/files/")
input: str = "The dog was chasing a cat into the house."
results = grammar_detector(input)  # Making use of the __call__ method

# Alternatively, extract the detectors
detectors: list[Detector] = grammar_detector.detectors
for detector in detectors:
    print(detector(input))Component: TokenThe smallest piece is thespacy.tokens.Tokenclass. EachTokenrepresents a single word and consists of a single JSON object. Alist[Token]represents a chain of words. Lists ofTokensare used inpatternsetYAML files to define grammatical patterns. EachTokencontains aPOS(part-of-speech), aTAG(tag), and/or aDEP(dependency). Grammatical categories are denoted withPOSandTAGwhile syntactic categories are denoted withDEP. AnOP(operation) may also be included to denote whether aTokenis required or optional. A complete list ofPOSs,TAGs, andDEPscan be found inthe spaCy glossary.Some examples ofPOSsare "VERB", "AUX", "NOUN", "PROPN", and "SYM" for symbol.Some examples ofTAGsare "VB" for base form verb, "VBD" for past tense verb, "VBG" for gerund/present participle verb, "VBN" for past participle verb, "VBP" for non-3rd person singular present verb, and "VBZ" for 3rd person singular present verb.Some examples ofDEPsare "ROOT" for root verb, "aux", "auxpass", "nsubj", and "dobj".Examples of TokensToken: Present Simple Verb# my_feature.yaml

patterns:
    # This is a single token (i.e. 1 word)
    - rulename: present simple verb
      tokens:  
        - {TAG: {IN: ["VBP", "VBZ"]},  # IN == one of theseToken: Passive Auxiliary Verb# my_feature.yaml

patterns:
    # This is also a single token (i.e. 1 word)
    - rulename: passive auxiliary
      tokens:  
        - {
          TAG: {IN: ["VBP", "VBZ"]},
          DEP: "auxpass",
          LEMMA: "be",
          OP: "+"
        }Token: Future Simple Be-going-to Passive# my_feature.yaml

patterns:
    # This is a list of 5 tokens (i.e. 5 words)
    - rulename: future simple be-going-to passive
      tokens:
        - {TAG: {IN: ["VBP", "VBZ"]}, DEP: "aux", OP: "+"}
        - {TAG: "VBG", OP: "+", LEMMA: "go"}
        - {TAG: "TO", DEP: "aux", OP: "+"}
        - {TAG: "VB", DEP: "auxpass", LEMMA: "be"}
        - {TAG: "VBN", OP: "+"}Token: Ditransitive/Trivalency# my_feature.yaml

patterns:
    # This is a list of 4 tokens minimum with some degree of recursivity
    - rulename: ditransitive
      tokens:
        - {DEP: "nsubj"}
        - {OP: "*"}  # Indicates possible filler words between the tokens
        - {DEP: "ROOT"}
        - {OP: "*"}
        - {DEP: {IN: ["dobj", "iobj", "pobj", "dative"]}}
        - {OP: "*"}
        - {DEP: {IN: ["dobj", "iobj", "pobj", "dative"]}}Component: PatternsEachPatterninpatternshas two properties:rulename: str-- the name given to thePatternwith the correspondinglist[spacy.tokens.Token]tokens: list[spacy.tokens.Token]-- the grammatical pattern# transitivity.yaml

config:
    how_many_matches: one

patterns:
    - rulename: ditransitive
      tokens:
          - {DEP: "nsubj"}
          - {OP: "*"}
          - {DEP: "ROOT"}
          - {OP: "*"}
          - {DEP: {IN: ["dobj", "iobj", "pobj", "dative"]}}
          - {OP: "*"}
          - {DEP: {IN: ["dobj", "iobj", "pobj", "dative"]}}

    - rulename: transitive
      tokens:
          - {DEP: "nsubj"}
          - {OP: "*"}
          - {DEP: "ROOT"}
          - {OP: "*"}
          - {DEP: "dobj"}

    - rulename: intransitive
      tokens:
          - {DEP: "nsubj", LOWER: {NOT_IN: ["it"]}}
          - {OP: "*"}
          - {DEP: "ROOT"}

    - rulename: impersonal
      tokens:
          - {TAG: "PRP", DEP: "nsubj", LOWER: "it"}
          - {OP: "*"}
          - {DEP: "ROOT"}Component: Patternset YAML FilesThepatternsetsexpand the capabilities of theGrammarDetectorto detect for new features. Thepatternsetsare created by loading YAML files containing these three properties:patterns: list[Pattern]-- an array of named sets of tokensconfig: dict[str, Union[str, bool]]-- a configuration object to modify input/outputtests: list[Test]-- an array of tests to validate the accuracy of thepatternsInternally, this data from thepatternsetfile is converted into aPatternSet.Patternset Files: Example for Active/Passive Voice# voices.yaml

config:
    how_many_matches: one

patterns:
    - rulename: active
      tokens:
          - {DEP: "aux", OP: "*"}
          - {DEP: "ROOT"}

    - rulename: passive
      tokens:
          - {DEP: "aux", OP: "*"}
          - {DEP: "auxpass", OP: "+"}
          - {TAG: "VBN", DEP: "ROOT"}

tests:
    - input: The cat was chased by the dog.
      rulenames:
          - passive
      spans:
          - was chased

    - input: The dog chased the cat.
      rulenames:
          - active
      spans:
          - chasedPatternset Files: 1) Defining Rules viapatternsThepatterns: list[dict[str, Union[str, list[spacy.tokens.Token]]]]list contains rules and grammatical patterns with the following properties:rulename: str-- the name of the grammatical pattern (e.g. "present simple")tokens: list[spacy.tokens.Token]-- the tokens of the grammatical patternPatternset Files: 2) Configuring viaconfigTheconfig: dict[str, Union[str, bool]]dict contains several options for modifying the input and/or output:extract_noun_chunks: bool-- if true, then fragment the input into nouns before running the detector (default false)how_many_matches: str-- if "all", then get all matches; if "one", then get the longest match (default "all")skip_tests: bool-- if true, then skip all tests in the file when running the unittests (default false)Patternset Files: 3) Testing viatestsThetests: list[dict[str, Union[str, bool, list[str]]]]list contains unittests with the following properties:input: str-- the sentence, clause, or phrase to be testedrulenames: list[str]-- the expected rulenamesspans: list[str]-- the expected matching textskip: bool-- if true, then skip this test (but not the others)Each test must contain 1) theinputand 2) therulenamesand/or thespans. To run the tests in yourpatternset, call theGrammarDetector.run_tests(self)instance method (see Usage: Running Tests in Custom Patternset YAML Files).Internal Component: PatternSet and PatternSetRepositoryThePatternSetRepositoryreads apatternsetYAML file and converts it into an internalPatternSet. The storedPatternSetscan be retrieved individually by referencing its name as the cache key or retrieved collectively as alist[PatternSet]. ThePatternSetRepositoryextends theRepository[Generic[T]]helper class for creating, caching, and querying.Internal Component: PatternSetMatcherThePatternSetMatcheris a wrapper class that is composed of an innerspacy.matcher.Matcherand logic to interpretPatternSets. The patterns defined in thePatternSetsare automatically loaded into the innerMatcher. The raw matches from the innerMatcherare then converted into a reader-friendly format.Internal Component: DetectorTheDetectoris the internal entrypoint by which a sentence, clause, or phrase is analyzed. ADetectorcontains onePatternSetand onePatternSetMatcher. EachDetectoris bound to the specific grammatical feature of thePatternSet. After loading theGrammarDetector, itsDetectorscan be accessed via theGrammarDetector.detectors: list[Detector]instance property. This permits running them manually and reusing them. TheGrammarDetectorandDetectorsare not bound to the text input.Internal Component: DetectorRepositoryTheDetectorRepositoryis responsible for creating and storingDetectors. It is wrapped by theGrammarDetectorclass, the main entrypoint. The repository manages thePatternSetRepositoryand loads itsPatternSetsinto thePatternSetMatchers. TheDetectorRepositoryextends theRepository[Generic[T]]helper class for creating, caching, and querying.ContributingThis tool is only as good as thepatternsetYAML files that support it. The primary ways to contribute to this project are:Creating new built-inpatternsetsImproving existingpatternsin the built-inpatternsetsAddingteststo the built-inpatternsetsAdding newconfigoptions and features to the codebaseCloning the repository:$ git clone https://github.com/SKCrawford/grammar-detector.gitPreparing the dev environment:$ pipenv shell
$ pipenv install --dev
$ python -m spacy download en_core_web_mdRunning theGrammarDetectorfrom the repository:$ python -m grammardetector "The dog was chasing a cat into the house."Running thepatternsetunittests from the repository:$ python -m unittestTo add new grammatical features or improve existing features, focus your efforts on thepatternsetsdirectory and its YAML files. You may find the token tables generated by theGrammarDetector.token_table(self, input: str, **kwargs)instance method to be helpful for conceptualizing sequences of tokens. You may also find the tokenlike lists generated by theGrammarDetector.token_data(self, input: str, **kwargs)instance method to be helpful when generating new patterns or improving upon existing patterns.Submissions ofpatternsetfiles will be rejected if they do not include tests for each pattern.AuthorsSteven Kyle CrawfordVersion History0.2.4New feature: Generate lists of tokens, which may be adapted for use in patternset files, via theGrammarDetector.token_data(self, input: str, **kwargs)instance method.Export theTokenlikereturn type for thetoken_datamethod for type safety.Improve docstrings for thetoken_dataandtoken_tablemethods in theGrammarDetectorclass and utilities package.0.2.3Rename the GrammarDetector constructor keyword argument from dataset to language_model.Change the default language model from en_core_web_lg to en_core_web_md0.2.2Rename the patternset file property from meta to config. Retain usage of meta internally to avoid confusion with the Config class.Rename the run_tests keyword argument from internal_tests to builtin_tests.Bugfix the repository's test suite runner.0.2.1Improve readme readability0.2.0Alpha release0.1.0Pre-alpha releaseLicenseThis project is licensed under the GNU General Public License V3. See the LICENSE.txt file for details.AcknowledgmentsspaCy- Free open-source library for Natural Language Processing in Python (license)