Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d963949b5 | |||
| 7badbed0cb | |||
| 04e4ef362e | |||
| d1ccf5919a | |||
| f1b542f93c | |||
| ac24bb48dd |
38
.gitea/workflows/regex-humanizer-cli.yml
Normal file
38
.gitea/workflows/regex-humanizer-cli.yml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout: 600
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.11'
|
||||||
|
cache: 'pip'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
python -m pip install -e .
|
||||||
|
python -m pip install pytest pytest-cov ruff
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: python -m pytest tests/ -v --tb=short
|
||||||
|
|
||||||
|
- name: Run linting
|
||||||
|
run: python -m ruff check regex_humanizer/
|
||||||
|
|
||||||
|
- name: Run type checking
|
||||||
|
run: python -m pip install mypy && python -m mypy regex_humanizer/ --ignore-missing-imports
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
"""Command-line interface for Regex Humanizer."""
|
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
from typing import Optional
|
|
||||||
import click
|
import click
|
||||||
from .parser import parse_regex
|
from .parser import parse_regex
|
||||||
from .translator import translate_regex
|
from .translator import translate_regex
|
||||||
from .test_generator import generate_test_cases
|
from .test_generator import generate_test_cases
|
||||||
from .flavors import get_flavor_manager, get_available_flavors
|
from .flavors import get_flavor_manager
|
||||||
from .interactive import start_interactive_mode
|
from .interactive import start_interactive_mode
|
||||||
|
|
||||||
|
|
||||||
@@ -194,7 +192,7 @@ def validate(ctx: click.Context, pattern: str, flavor: str):
|
|||||||
click.echo(f"AST node count: {len(get_all_nodes(ast))}")
|
click.echo(f"AST node count: {len(get_all_nodes(ast))}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
click.echo(f"\nPattern: {pattern}")
|
click.echo(f"\nPattern: {pattern}")
|
||||||
click.echo(f"Validation: FAILED")
|
click.echo("Validation: FAILED")
|
||||||
click.echo(f"Error: {e}")
|
click.echo(f"Error: {e}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
"""Interactive REPL mode for exploring regex patterns."""
|
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
from typing import Optional
|
|
||||||
from .parser import parse_regex
|
|
||||||
from .translator import translate_regex
|
from .translator import translate_regex
|
||||||
from .test_generator import generate_test_cases
|
from .test_generator import generate_test_cases
|
||||||
from .flavors import get_flavor_manager
|
from .flavors import get_flavor_manager
|
||||||
@@ -51,14 +47,14 @@ class InteractiveSession:
|
|||||||
os.makedirs(os.path.dirname(self.history_file), exist_ok=True)
|
os.makedirs(os.path.dirname(self.history_file), exist_ok=True)
|
||||||
with open(self.history_file, 'w') as f:
|
with open(self.history_file, 'w') as f:
|
||||||
for cmd in self.history[-1000:]:
|
for cmd in self.history[-1000:]:
|
||||||
f.write(cmd + '\\n')
|
f.write(cmd + '\n')
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""Run the interactive session."""
|
"""Run the interactive session."""
|
||||||
print("\\nRegex Humanizer - Interactive Mode")
|
print("\nRegex Humanizer - Interactive Mode")
|
||||||
print("Type 'help' for available commands, 'quit' to exit.\\n")
|
print("Type 'help' for available commands, 'quit' to exit.\n")
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
@@ -79,7 +75,7 @@ class InteractiveSession:
|
|||||||
self._process_command(user_input.strip())
|
self._process_command(user_input.strip())
|
||||||
|
|
||||||
except (KeyboardInterrupt, EOFError):
|
except (KeyboardInterrupt, EOFError):
|
||||||
print("\\nGoodbye!")
|
print("\nGoodbye!")
|
||||||
break
|
break
|
||||||
|
|
||||||
def _process_command(self, command: str):
|
def _process_command(self, command: str):
|
||||||
@@ -148,10 +144,10 @@ Examples:
|
|||||||
result = translate_regex(pattern, self.flavor)
|
result = translate_regex(pattern, self.flavor)
|
||||||
|
|
||||||
header = f"Pattern: {pattern}"
|
header = f"Pattern: {pattern}"
|
||||||
print("\\n" + "=" * (len(header)))
|
print("\n" + "=" * (len(header)))
|
||||||
print(header)
|
print(header)
|
||||||
print("=" * (len(header)))
|
print("=" * (len(header)))
|
||||||
print("\\nEnglish Explanation:")
|
print("\nEnglish Explanation:")
|
||||||
print("-" * (len(header)))
|
print("-" * (len(header)))
|
||||||
print(result)
|
print(result)
|
||||||
print()
|
print()
|
||||||
@@ -170,17 +166,17 @@ Examples:
|
|||||||
result = generate_test_cases(pattern, self.flavor, 3, 3)
|
result = generate_test_cases(pattern, self.flavor, 3, 3)
|
||||||
|
|
||||||
header = f"Pattern: {pattern}"
|
header = f"Pattern: {pattern}"
|
||||||
print("\\n" + "=" * (len(header)))
|
print("\n" + "=" * (len(header)))
|
||||||
print(header)
|
print(header)
|
||||||
print("=" * (len(header)))
|
print("=" * (len(header)))
|
||||||
print(f"\\nFlavor: {self.flavor}")
|
print(f"\nFlavor: {self.flavor}")
|
||||||
|
|
||||||
print("\\nMatching strings:")
|
print("\nMatching strings:")
|
||||||
print("-" * (len(header)))
|
print("-" * (len(header)))
|
||||||
for i, s in enumerate(result["matching"], 1):
|
for i, s in enumerate(result["matching"], 1):
|
||||||
print(f" {i}. {s}")
|
print(f" {i}. {s}")
|
||||||
|
|
||||||
print("\\nNon-matching strings:")
|
print("\nNon-matching strings:")
|
||||||
print("-" * (len(header)))
|
print("-" * (len(header)))
|
||||||
for i, s in enumerate(result["non_matching"], 1):
|
for i, s in enumerate(result["non_matching"], 1):
|
||||||
print(f" {i}. {s}")
|
print(f" {i}. {s}")
|
||||||
@@ -266,17 +262,17 @@ Examples:
|
|||||||
def _cmd_example(self, args: str):
|
def _cmd_example(self, args: str):
|
||||||
"""Show an example pattern."""
|
"""Show an example pattern."""
|
||||||
examples = [
|
examples = [
|
||||||
r"^\\d{3}-\\d{4}$",
|
r"^\d{3}-\d{4}$",
|
||||||
r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
|
r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
|
||||||
r"^(?:http|https)://[^\\s]+$",
|
r"^(?:http|https)://[^\s]+$",
|
||||||
r"\\b\\d{4}-\\d{2}-\\d{2}\\b",
|
r"\b\d{4}-\d{2}-\d{2}\b",
|
||||||
r"(?i)(hello|hi|greetings)\\s+world!?",
|
r"(?i)(hello|hi|greetings)\s+world!?",
|
||||||
]
|
]
|
||||||
|
|
||||||
import random
|
import random
|
||||||
example = random.choice(examples)
|
example = random.choice(examples)
|
||||||
print(f"\\nExample pattern: {example}")
|
print(f"\nExample pattern: {example}")
|
||||||
print("\\nType: explain " + example)
|
print("\nType: explain " + example)
|
||||||
print("Type: test " + example)
|
print("Type: test " + example)
|
||||||
print()
|
print()
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
"""Regex parser for converting regex patterns to AST nodes."""
|
from typing import Optional, Any
|
||||||
|
|
||||||
from typing import Optional, Union, Any
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
@@ -355,7 +353,7 @@ class RegexParser:
|
|||||||
self.pos = end + 1
|
self.pos = end + 1
|
||||||
return RegexNode(
|
return RegexNode(
|
||||||
node_type=NodeType.UNICODE_PROPERTY,
|
node_type=NodeType.UNICODE_PROPERTY,
|
||||||
raw=f'\\p{{{prop}}}')
|
raw=f'\\p{{{prop}}}',
|
||||||
position=self.pos - len(f'\\p{{{prop}}}')
|
position=self.pos - len(f'\\p{{{prop}}}')
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -558,7 +556,6 @@ class RegexParser:
|
|||||||
is_non_capturing=False
|
is_non_capturing=False
|
||||||
)
|
)
|
||||||
elif next_char in 'iDsx':
|
elif next_char in 'iDsx':
|
||||||
flag = next_char
|
|
||||||
self.pos += 1
|
self.pos += 1
|
||||||
children = self._parse_sequence()
|
children = self._parse_sequence()
|
||||||
return RegexNode(
|
return RegexNode(
|
||||||
@@ -592,7 +589,6 @@ class RegexParser:
|
|||||||
|
|
||||||
if char in '*+?':
|
if char in '*+?':
|
||||||
self.pos += 1
|
self.pos += 1
|
||||||
quant_type = char
|
|
||||||
if char == '*':
|
if char == '*':
|
||||||
min_count = 0
|
min_count = 0
|
||||||
max_count = float('inf')
|
max_count = float('inf')
|
||||||
@@ -634,7 +630,6 @@ class RegexParser:
|
|||||||
is_possessive = True
|
is_possessive = True
|
||||||
self.pos += 1
|
self.pos += 1
|
||||||
|
|
||||||
quant_type = '{' + quant_content + '}'
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
"""Test case generator for regex patterns."""
|
|
||||||
|
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
from typing import Optional, Callable
|
from typing import Optional
|
||||||
from .parser import parse_regex, RegexNode, NodeType
|
from .parser import parse_regex, RegexNode, NodeType
|
||||||
|
|
||||||
|
|
||||||
@@ -120,12 +118,13 @@ class TestCaseGenerator:
|
|||||||
"""Generate strings that do NOT match the pattern."""
|
"""Generate strings that do NOT match the pattern."""
|
||||||
try:
|
try:
|
||||||
ast = parse_regex(pattern, self.flavor)
|
ast = parse_regex(pattern, self.flavor)
|
||||||
return self._generate_non_matching_from_ast(ast, count, max_length)
|
return self._generate_non_matching_from_ast(pattern, ast, count, max_length)
|
||||||
except Exception:
|
except Exception:
|
||||||
return self._generate_fallback_non_matching(pattern, count)
|
return self._generate_fallback_non_matching(pattern, count)
|
||||||
|
|
||||||
def _generate_non_matching_from_ast(
|
def _generate_non_matching_from_ast(
|
||||||
self,
|
self,
|
||||||
|
pattern: str,
|
||||||
node: RegexNode,
|
node: RegexNode,
|
||||||
count: int,
|
count: int,
|
||||||
max_length: int
|
max_length: int
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
"""Translator for converting regex AST to human-readable English."""
|
|
||||||
|
|
||||||
from typing import Optional
|
|
||||||
from .parser import (
|
from .parser import (
|
||||||
RegexNode, NodeType, LiteralNode, CharacterClassNode,
|
RegexNode, NodeType, LiteralNode, CharacterClassNode,
|
||||||
QuantifierNode, GroupNode, RegexParser
|
QuantifierNode, GroupNode, RegexParser
|
||||||
@@ -282,7 +279,7 @@ class RegexTranslator:
|
|||||||
|
|
||||||
def _translate_backreference(self, node: RegexNode) -> str:
|
def _translate_backreference(self, node: RegexNode) -> str:
|
||||||
"""Translate a backreference."""
|
"""Translate a backreference."""
|
||||||
return f"same as capture group \\\{node.raw}"
|
return f"same as capture group \\{node.raw}"
|
||||||
|
|
||||||
|
|
||||||
def translate_regex(pattern: str, flavor: str = "pcre") -> str:
|
def translate_regex(pattern: str, flavor: str = "pcre") -> str:
|
||||||
|
|||||||
Reference in New Issue
Block a user