1
\$\begingroup\$

This is a much more basic implementation of Regular Expressions (Python docs). Do note that core functionality is missing and the functions aren't completely fool-proof, but it does a good job of matching basic patterns. Also, filtering 'symmetric' patterns (like quotation marks "" or markdown bold **) is not possible.

That being said, here's the code:

"""
Module for quick and efficient pattern filtering.
Similar to Regular Expressions (https://docs.python.org/3.3/library/re.html);
but with a goal of being easier to use. Note that lots of features that are available in re
are *not* available in this module. An example of supported operations:

- Filtering everything in parentheses () in a string
- Filtering every first occurrence of a word in parentheses () or square brackets []
- Tagging every verb in a string with angle brackets <> (given a list of verbs!)

An example of unsupported operations:

- Filtering email addresses from text 
- Retrieving text embedded in quotation marks '' 
- Complex text / lexical analysis
"""

class Crucial:
    """Basic interface for pattern matching and tagging. 
    You are recommended to use the Scope class for interacting with this interface.
    Contents:

    * _asymmetric(): Asymmetric pattern matching
    * _tag(): Asymmetric pattern tagging
    * _beautify(): Beautify results (append to list)
    """
    def __init__(self):
        pass

    def _asymmetric(self, _raw, _start, _end, _filter_first):
        """Provides a backend interface for asymmetric pattern matching.
        Works for a 'raw' string, along with a single start / end character.
        Arguments:

        * _raw: String containing patterns to filter (e.g. 'spam [eggs]')
        * _start: Pattern start character (e.g. '[')
        * _end: Pattern end character  (e.g. ']')
        * _filter_first: Argument to filter only the first pattern found.
        """
        final = []
        temp, sort = [], []

        for pos, val in enumerate(_raw):
            if val == _start:
                temp.append([pos])
            elif val == _end:
                next = True
                for index in temp:
                    if _raw[index[0]] == _start:
                        next = False
                        # This avoids match collisions (missing end character)
                    if not next:
                        temp[temp.index(max(temp))].append(pos)
                        # Find the latest (max) pattern
                        sort.append(max(temp))
                        temp.pop(temp.index(max(temp)))

        for val in sorted(sort):
            final.append(_raw[val[0] + 1:val[-1]])
            if _filter_first:
                break
        return final

    def _tag(self, _raw, _tags, _seperator):
        """Provides a backend interface for asymmetric pattern tagging.
        Works for a 'raw' string, along with a tag dictionary and seperator character.
        Arguments:

        * _raw: String containing all words, not just those to be matched.
        * _tags: Dictionary containing key:value pairs like "spam":"[]" where every
                 "spam" will be enclosed by "[]" ("[spam]")
        * _seperator: Word seperator, commonly " " or ", " for a comma-seperated list.
        """

        final = ""

        for val in _raw.split(_seperator):
            if val not in _tags:
                final += val + _seperator 
            else:
                final += _tags[val][0] + val + _tags[val][1]
                final += _seperator

        return final

    def _beautify(self, _filtered, _allow_nested):
        """Beautify filtered patterns by appending them to a list.
        Works for 'filtered' dictionaries.
        Arguments:

        * _filtered: Dictionary with results matched by start / end character
        * _allow_nested: Boolean, if True, all results will be returned in a list or printed.
                     Else, only 'pure' results (results containing no nested patterns), are returned.
        """

        def is_nested(frm):
            # Check if a frame is pure, treating it as 'raw'
            result = Scope().auto(frm)
            for pattern_ in result:
                for frame_ in result[pattern_]:
                    if len(frame_) > 0:
                        return True
            return False 

        final = []
        for pattern in _filtered:
            for frame in _filtered[pattern]:
                if not _allow_nested:
                    if not is_nested(frame):
                        final.append(frame)
                        continue
                    continue
                final.append(frame)
        return final       


class Scope:
    """Main frontend interface for pattern matching / tagging.
    Contents:

    * _err(): Function for constructing error messages
    * match(): Allows asymmetric pattern matching
    * tag(): Tag specific words in a raw string
    * auto(): Automatically match patterns
    * beautify(): Wrapper for backend _beautify()
    """

    def __init__(self):
        self.RED = "\033[91m"
        self.GRN = "\033[92m"
        self.YLW = "\033[93m"
        self.WHT = "\033[00m"

    def _err(self, message):
        """Print errors to the screen."""
        print("{}{}{}".format(self.RED, message, self.WHT))

    def match(self, raw, *args, filter_first=False):
        """Match patterns based on asymmetric characters.
        match() will always return a dictionary with key:value pairs like
        pattern_start + pattern_end : [returned, items, seperatd, with, commas].
        Arguments:

        * raw: A raw string to be inspected for patterns
        * *args: Any amount of arguments (patterns), in the form of
                 "<>", "[]", "()". For example: match("spam [eggs]", "[]")
        * filter_frames: Optional argument to filter only the first frame of each pattern.
        """
        final = {}

        for arg in args:
            # arg represents a pattern.
            start = arg[0]
            end = arg[1]

            collision = (start + end in args and end + start in args)
            if collision:
                self._err("Collision: {}{} / {}{}".format(start, end, end, start))
                return

            final[start + end] = Crucial()._asymmetric(raw, start, end, filter_first)
        return final

    def tag(self, raw, tags, seperator=" "):
        """Encapsulate words based on a 'tags' enum.
        Arguments:

        * raw: A raw string containing words to be tagged
        * tags: Dictionary containing key:value pairs like "spam":"[]".
                Each "spam" in raw will be surrounded by "[" ... "]".
        * seperator: Optional argument for seperating words (string.split()).
                     Defaults to a single space " ".
        """
        return Crucial()._tag(raw, tags, seperator)

    def auto(self, raw):
        """Automatically match frames in a string, based on common patterns.
        auto() will always return a dictionary with key:value pairs like
        pattern_start + pattern_end : [returned, items, seperatd, with, commas].
        Arguments:

        * raw: A raw string to be inspected for patterns
        """
        final = {}
        temp = []

        for pattern in ["()", "[]", "<>", "{}"]:
            if pattern[0] in raw and pattern[1] in raw:
                result = Scope().match(raw, pattern)
                for pattern_ in result:
                    final[pattern_] = result[pattern_]

        return final

    def beautify(self, filtered, allow_nested=True):
        """Beautify matched patterns.
        Arguments:

        * filtered: Dictionary containing filtered patterns
        * allow_nested: If set to True, nested patterns are allowed.
                        Else, only 'pure' patterns are used.
        """
        return Crucial()._beautify(filtered, allow_nested)

And here's some examples:

import scope
scope = scope.Scope()

easy = "The (quick) (grey) [fox] jumps over the (lazy) (white) [dog]"

hard = """
This is (level 0 and (this is level 1. (Level 2 is especially (interesting) for pattern matching!) 
[this is additional information, although it doesn't help you understand what is going on.))
"""

print(scope.match(easy, "()"))
# Match adjectives

print(scope.match(easy, "[]"))
# Match nouns

print(scope.auto(easy))
# Match both nouns and adjectives

print(scope.beautify(scope.auto(easy)))
# Convert the results to a list and print it

print(scope.beautify(scope.auto(hard), allow_nested=False)) 
# Print only 'pure' patterns

print(scope.tag(hard, {"is":"<>"}))
# Tag all verbs 'is' with angle brackets
# etc.

I would appreciate any advice / critisism regarding coding conventions or style, performance, usability, etc.

\$\endgroup\$
3
\$\begingroup\$

Some high-level thoughts and notes:

  • I think you don't really need to have leading underscores in the method argument names
  • there is a typo: _seperator -> _separator (the typo is consistent in both the code and the docstrings)
  • you can unpack arg in a loop, replacing:

    for arg in args:
        # arg represents a pattern.
        start = arg[0]
        end = arg[1]
    

    with:

    for start, end in args:
    
  • shadowing built-in function: next is shadowed in the _asymmetric() method

  • temp variable is unused inside the auto() method
  • I would probably move the "color"-constants from being instance variables into the scope of the module or to a scope of a class itself:

    class Scope:
        RED = "\033[91m"
        GRN = "\033[92m"
        YLW = "\033[93m"
        WHT = "\033[00m"
    

Overall, my feeling was that you actually don't need classes here - most of the methods you use can actually be static and you don't have instance variables - there is basically no "state" shared across the methods:

\$\endgroup\$
  • \$\begingroup\$ Does this mean simply using @staticmethod would be good enough, or should I abandon the use of classes altogether (and if so, how can I keep the code well organized?)? \$\endgroup\$ – Daniel Jun 26 '17 at 13:54
  • 1
    \$\begingroup\$ @Coal_ I am also struggling with the best and the clearest indication of what is better - have a class that acts as a bucket - used to organize and group related things together, or to have a module/package for the same purpose. There is this topic eventually closed as opinion-based; also see this topic. I tend to think of a module for your use-case. \$\endgroup\$ – alecxe Jun 26 '17 at 13:59

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.