Package: rgx
Many people complain about unreadable and complex syntax of regular expressions.Many others complain about how they can't remember all constructs and features.rgxsolves those problems: it is a straightforward regexp builder. It also places non-capturing groups where needed to respect intended operator priority.It can produce a regular expression string to use inre.compileor any other regex library of your choice.In other words, withrgxyou can build a regular expression from parts, using straightforward and simple expressions.Installationpip install rgxThat's it.Basic usageHello, regex worldfromrgximportpattern,metaimportreseparator=meta.WHITESPACE.some()+(meta.WHITESPACE|",")+meta.WHITESPACE.some()# matches "hello world", "hello, world", "hello            world", "hello,world", "hello ,  world"hello_world=pattern(("hello",separator,"world"))# (?:hello(?:\s)*(?:\s|,)(?:\s)*world)re.compile(hello_world.render_str("i")# global flag (case-insensitive))Match some integersthis regex will match valid Python integer literals:fromrgximportpatternimportrenonzero=pattern("1").to("9")# [1-9]zero="0"digit=zero|nonzero# [0-9]integer=zero|(nonzero+digit.some())# 0|[1-9][0-9]*int_regex=re.compile(str(integer))...or this one:fromrgximportpattern,metaimportrenonzero=pattern("1").to("9")# [1-9]digit=meta.DIGIT# \dinteger=digit|(nonzero+digit.some())# \d|[1-9]\d*int_regex=re.compile(str(integer))Quickstartin this readme,xmeans some pattern object. Occasionaly,yis introduced to mean some other pattern object (or literal)Literals and pattern objectsrgxoperates mostly on so-called "pattern objects" —rgx.entities.RegexPatternistances.Your starting point would bergx.pattern— it creates pattern objects from literals (and from pattern objects, which doesn't make a lot of sense).rgx.pattern(str, escape: bool = True)creates a literal pattern — one that exactly matches given string. If you want to disable escaping, passescape=Falsergx.pattern(tuple[AnyRegexPattern])creates a non-capturing group of patterns (nested literals will be converted too)rgx.pattern(list[str])creates a character class (for example,rgx.pattern(["a", "b", "c"])creates pattern[abc], that matches any character of those in brackets)Same can be achieved byrgx.pattern("a").to("c")orrgx.pattern("a") | "b" | "c"Most operations with pattern objects support using Python literals on one side, for example:rgx.pattern("a") | bwould produce[ab]pattern object (specifically,rgx.entities.Chars)Rendering patternsfromrgximportpatternx=pattern("one")y=pattern("two")p=x|yrendered_with_str=str(p)# "one|two"rendered_with_method=p.render_str()# "one|two"rendered_with_method_flags=p.render_str("im")# (?im)one|twoCapturing Groupsfromrgximportpattern,reference,namedx=pattern("x")print(x.capture())# (x)print(reference(1))# \1named_x=x.named("some_x")# x.named(name: str)print(named_x)# (?P<some_x>x)named_x_reference=named("some_x")print(named_x_reference)# (?P=x)To create a capturing group, usex.capture(), orrgx.reference(group: int)for a reference.To create a named capturing group, usergx.named(name: str, x), orrgx.named(name: str)for a named reference.Character classesfromrgximportpattern,metaaz=pattern("a").to("z")# rgx.Chars.to(other: str | Literal | Chars)print(az)# [a-z]digits_or_space=pattern(["1","2","3",meta.WHITESPACE])print(digits_or_space)# [123\s]print(az|digits_or_space)# [a-z123\s]print(# rgx.Chars.reverse(self)(az|digits_or_space).reverse()# [^a-z123\s])Excluding charactersIf you have two instances of Chars (or compatible literals), you can exclude one from another:fromrgximportpatternletters=pattern("a").to("z")|pattern("A").to("Z")# [A-Za-z]vowels=pattern(list("aAeEiIoOuU"))# [AEIOUaeiou]consonants=letters.exclude(vowels)# [BCDFGHJ-NP-TV-Zbcdfghj-np-tv-z]Conditional patternfromrgximportpattern,conditionalx=pattern("x")y=pattern("y")z=pattern("z")capture=x.capture()# (x)(?(1)y|z)print(capture+conditional(1,y,z))Repeating patternsIf you need to match a repeating pattern, you can usepattern.repeat(count, lazy):a=pattern("a")a.repeat(5)# a{5}# ora*5# a{5}, multiplication is an alias for .repeata.repeat(5).or_more()# a{5,}a.repeat(5).or_less()# a{,5}a.repeat_from(4).to(5)# a{4, 5}, .repeat_from is just an alias for .repeat# ora.repeat(4)>>5# a{4, 5}a.repeat(1).or_less()# a?# or-a.repeat(1)# a?# ora.maybe()# a?a.repeat(1).or_more()# a+# or+a.repeat(1)# a+# or+a# a+# ora.many()# a+a.repeat(0).or_more()# a*# or+a.repeat(0)# a*# ora.some()# a*# or (what)+-(a*38)# a*Here's what's going on:pattern.repeat(count, lazy)returns a{count, count}Rangeobjectpattern * countis the same aspattern.repeat(count, False)Rangeimplementsor_more,or_lessandtomethods:Range.or_more()[or+Range] moves (on a copy) upper bound of range to infinity (actuallyNone)Range.or_less()[or-Range] moves (on a copy) lower bound of range to 0Range.to(count)[orRange >> count(right shift)] replaces upper bound with given numberAlso, RegexPattern implements unary plus (+pattern) as an alias forpattern.many()DocsPattern methodspattern.render_str(flags: str = '') -> strRenders given pattern into a string with specified global flags.pattern.set_flags(flags: str) -> LocalFlagsThis method adds local flags to given patternx.flags("y")# "(?y:x)"pattern.concat(other: AnyRegexPattern) -> ConcatUse to match one pattern and then another.A.concat(B)is equivalent toA + B(works if either A or B is a RegexPart object, not a Python literal)x.concat(y)# "xy"x+y# "xy"pattern.option(other: AnyRegexPattern) -> Chars | ReversedChars | OptionUse to match either one pattern or another.A.option(B)is equivalent toA | B(if either A or B is a RegexPart object, not a Python literal)x.option(y)# "x|y"x|y# "x|y"pattern.many(lazy: bool = False) -> RangeUse this for repeating patterns (one or more times)When not lazy, matches as many times as possible, otherwise matches as few times as possible.x.many()# "x+"x.many(True)# "x+?"pattern.some(lazy: bool = False) -> RangeUse this for repeating optional patterns (zero or more times)When not lazy, matches as many times as possible, otherwise matches as few times as possible.x.some()# "x*"x.some(True)# "x*?"pattern.maybe(lazy: bool = False) -> RangeUse this for optional patterns (zero or one times)When not lazy, matches as many times as possible, otherwise matches as few times as possible.x.maybe()# "x?"x.maybe(True)# "x??"pattern.x_or_less_times(count: int, lazy: bool = False) -> RangeUse this to match pattern x or less times (hence the name).When not lazy, matches as many times as possible, otherwise matches as few times as possible.x.x_or_less_times(5)# "x{,5}"x.x_or_less_times(5,True)# "x{,5}?"pattern.x_or_more_times(count: int, lazy: bool = False) -> RangeUse this to match pattern x or more times (hence the name).When not lazy, matches as many times as possible, otherwise matches as few times as possible.x.x_or_more_times(5)# "x{5,}"x.x_or_more_times(5,True)# "x{5,}?"pattern.x_times(count: int, lazy: bool = False) -> RangeUse this to match pattern exactly x times (hence the name).When not lazy, matches as many times as possible, otherwise matches as few times as possible.x.x_times(5)# "x{5}"x.x_times(5,True)# "x{5}?"x.repeat(5)# x{5}pattern.between_x_y_times(min_count: int, max_count: int, lazy: bool = False) -> RangeUse this to match pattern between x and y times, inclusive (hence the name).When not lazy, matches as many times as possible, otherwise matches as few times as possible.x.between_x_y_times(5,6)# "x{5,6}"x.between_x_y_times(5,6,True)# "x{5,6}?"pattern.lookahead(other: RegexPattern) -> ConcatUse this to indicate that given pattern occurs before some another pattern (lookahead).In other words,x.lookahead(y)matches a patternxonly if there isyafter itLookahead pattern won't be captured.x.lookahead(y)# x(?=y)x.before(y)# x(?=y)pattern.negative_lookahead(other) -> ConcatUse this to indicate that given pattern doesn't occur before some another pattern (negative lookahead).In other words,x.negative_lookahead(y)matches a patternxonly if there is noyafter itLookahead pattern won't be captured.x.negative_lookahead(y)# x(?!y)x.not_before(y)# x(?!y)pattern.lookbehind(other: RegexPattern) -> ConcatUse this to indicate that given pattern occurs after some another pattern (lookbehind).In other words,x.lookbehind(y)matches a patternxonly if there isybefore itLookbehind pattern won't be captured.x.lookbehind(y)# (?<=y)xx.after(y)# (?<=y)xpattern.negative_lookbehind(other) -> ConcatUse this to indicate that given pattern goes before some another pattern (negative lookbehind).In other words,x.negative_lookbehind(y)matches a patternxonly if there is NOybefore itLookbehind pattern won't be captured.x.negative_lookbehind(y)# (?<!y)xx.not_after(y)# (?<!y)xpattern.capture() -> GroupUse this to make a capturing group out of pattern.x.capture()# (x)Metargx.metais a collection of different meta-sequences and anchors:meta.WORD_CHAR=UnescapedLiteral(r"\w")meta.NON_WORD_CHAR=UnescapedLiteral(r"\W")meta.DIGIT=UnescapedLiteral(r"\d")meta.NON_DIGIT=UnescapedLiteral(r"\D")meta.WHITESPACE=UnescapedLiteral(r"\s")meta.NON_WHITESPACE=UnescapedLiteral(r"\S")meta.WORD_BOUNDARY=UnescapedLiteral(r"\b")meta.NON_WORD_BOUNDARY=UnescapedLiteral(r"\B")meta.ANY=UnescapedLiteral(".")meta.NEWLINE=UnescapedLiteral(r"\n")meta.CARRIAGE_RETURN=UnescapedLiteral(r"\r")meta.TAB=UnescapedLiteral(r"\t")meta.NULL_CHAR=UnescapedLiteral(r"\0")meta.STRING_START=UnescapedLiteral("^")meta.STRING_END=UnescapedLiteral("$")Alsorgx.meta.CHAR_ESCAPE(char_number: int)is available:fromrgximportmetaprint(meta.CHAR_ESCAPE(32))# \x20print(meta.CHAR_ESCAPE(320))# \u0140print(meta.CHAR_ESCAPE(320000))# \U0004e200Unicode metargx.unicode_metais a collection of functions and constants, mostly for\pand\Pusage:Functions:unicode_meta.PROPERTY(value:str)# renders into `\p{value}` (any character with property specified by value, e.g. `PROPERTY("Ll") -> \p{Ll}`)unicode_meta.PROPERTY_INVERSE(value:str)# matches all characters *not* matched by corresponding `PROPERTY` (`\P{value}`)unicode_meta.NAMED_PROPERTY(name:str,value:str)# renders into `\p{name=value}` and matches any character which property `name` equals `value`unicode_meta.NAMED_PROPERTY_INVERSE(name:str,value:str)# same, but inverted (`\P{name=value}`)Constants:unicode_meta.LETTER=PROPERTY("L")unicode_meta.NON_LETTER=PROPERTY_INVERSE("L")unicode_meta.WHITESPACE=PROPERTY("Z")unicode_meta.NON_WHITESPACE=PROPERTY_INVERSE("Z")unicode_meta.DIGIT=PROPERTY("Nd")unicode_meta.NON_DIGIT=PROPERTY("Nd")ExtendingYou can extend generation by subclassing one of the classes ofrgx.entitiesmodule.The one neccessary method to provide is.render(self, context: rgx.Context). It should return an iterable of strings (e.g.["something"]).Built-in components (and this section) are using generators for that purpose, but you're free to choose whatever works for you.
For example, if you want to render a PCRE accept control verb -(*ACCEPT), you can do it like this:fromrgx.entitiesimportRegexPattern,Concatfromrgximportpattern,ContextfromtypingimportIterableclassAccept(RegexPattern):defrender(self,context:Context)->Iterable[str]:yield"(*ACCEPT)"defaccept(self)->Concat:returnself+Accept()RegexPattern.accept=acceptx=pattern("something").accept()print(x)# something(*ACCEPT)Or like this:fromrgx.entitiesimportRegexPattern,Concatfromrgximportpattern,ContextfromtypingimportIterableclassAccept(RegexPattern):def__init__(self,accepted_pattern:RegexPattern):self.accepted_pattern=accepted_patterndefrender(self,context:Context)->Iterable[str]:yield fromaccepted_pattern.render(context)yield"(*ACCEPT)"defaccept(self)->Accept:returnAccept(self)RegexPattern.accept=acceptx=pattern("something").accept()# something(*ACCEPT)PriorityIf your extension has to rely on some priority, you can userespect_priorityfunction.Let's say you want to add ax/yoperation, which does something (wow) and has prority betweena|bandab— soa|b/cdis the same asa|(?:b/(?:cd)).fromrgx.entitiesimportRegexPattern,Concat,Option,AnyRegexPattern,respect_priority,pattern,ContextfromtypingimportIterableclassMagicSlash(RegexPattern):priority=(Concat.priority+Option.priority)//2# let's take something in the middledef__init__(self,left:RegexPattern,right:RegexPattern):self.left=respect_priority(left,self.priority)# you need to wrap all parts of your expression in respect_priority()self.right=respect_priority(right,self.priority)# ...and pass your expression priority as a second argumentdefrender(self,context:Context)->Iterable[str]:yield fromself.left.render(context)yield"/"yield fromself.right.render(context)defslash(self,other:AnyRegexPattern)->MagicSlash:# AnyRegexPattern is either a RegexPattern instance or a Python literalreturnMagicSlash(self,other)# respect_priority already takes literals in consideration, so no extra actions neededdefrslash(self,other:AnyRegexPattern)->MagicSlash:# other/selfother=pattern(other)returnother/selfRegexPattern.slash=slashRegexPattern.__truediv__=slash# / operatorRegexPattern.__rtruediv__=rslasha=pattern("a")b=pattern("b")c=pattern("c")d=pattern("d")print((a|b)/(c+d)# [ab]/cd)print(((a|b)/c)+d# (?:[ab]/c)d)print(a|(b/c)+d# a|(?:b/c)d)Common questionsDifference between(x, y)andx + yPrevious examples used()and+, and the difference might not be so obvious.x + ycreates a concatenation of patterns (rgx.entities.Concat), with no extra characters apart from those of patternsx + ycan be used only if at least one of the operands is a pattern object (that is, created with one ofrgxfunctions or is one ofrgxconstants)x + yproduces a pattern object itself, so you won't need to callpatternon it to call pattern methodspattern((x, y))creates a non-capturing group (rgx.entities.NonCapturingGroup):pattern((x, y)).render_str()->(?:xy)(x, y)can be used with any pattern-like literals or pattern objects(x, y)is a tuple literal, so you can't use pattern methods on it directly or convert it into a complete expression (you need to usergx.patternon it first)