Compare commits

6 Commits
v1.0.0 ... main

Author SHA1 Message Date
6d963949b5 fix: resolve CI/CD issues and linting errors
Some checks failed
CI / test (push) Failing after 3h0m45s
CI / build (push) Has been cancelled
2026-02-06 01:26:35 +00:00
7badbed0cb fix: resolve CI/CD issues and linting errors
Some checks failed
CI / test (push) Has been cancelled
CI / build (push) Has been cancelled
2026-02-06 01:26:34 +00:00
04e4ef362e fix: resolve CI/CD issues and linting errors
Some checks failed
CI / test (push) Has been cancelled
CI / build (push) Has been cancelled
2026-02-06 01:26:34 +00:00
d1ccf5919a fix: resolve CI/CD issues and linting errors
Some checks failed
CI / build (push) Has been cancelled
CI / test (push) Has been cancelled
2026-02-06 01:26:33 +00:00
f1b542f93c fix: resolve CI/CD issues and linting errors
Some checks failed
CI / test (push) Has been cancelled
CI / build (push) Has been cancelled
2026-02-06 01:26:33 +00:00
ac24bb48dd fix: resolve CI/CD issues and linting errors
Some checks failed
CI / build (push) Has been cancelled
CI / test (push) Has been cancelled
2026-02-06 01:26:33 +00:00
6 changed files with 64 additions and 41 deletions

View 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

View File

@@ -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)

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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: