Skip to content

Commit

Permalink
Add name validation and listing to Namespace
Browse files Browse the repository at this point in the history
- Added a method to validate names based on a regular expression.
- Added a method to list all names under a given name.
- Updated the get_path method to use the new name validation method.
- Added tests for the new methods.
  • Loading branch information
basicthinker committed Aug 20, 2023
1 parent 4019c42 commit 579398b
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 1 deletion.
55 changes: 54 additions & 1 deletion devchat/namespace.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
from typing import List, Optional
import re


class Namespace:
Expand All @@ -12,6 +13,22 @@ def __init__(self, root_path: str,
self.root_path = root_path
self.branches = branches if branches else ['usr', 'org', 'sys']

@staticmethod
def is_valid_name(name: str) -> bool:
"""
Check if a name is valid.
A valid name is either an empty string or
a sequence of one or more alphanumeric characters, hyphens, or underscores,
separated by single dots. Each component cannot contain a dot.
:param name: The name to check.
:return: True if the name is valid, False otherwise.
"""
# The regular expression pattern for a valid name
pattern = r'^$|^(?!.*\.\.)[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)*$'
return bool(re.match(pattern, name))

def get_path(self, name: str) -> Optional[str]:
"""
:param name: The command name in the namespace.
Expand All @@ -21,11 +38,47 @@ def get_path(self, name: str) -> Optional[str]:
return None
# Convert the dot-separated name to a path
path = os.path.join(*name.split('.'))

for branch in self.branches:
full_path = os.path.join(self.root_path, branch, path)
if os.path.exists(full_path):
# If it exists, return the branch/path part
return os.path.join(branch, path)
# If no existing path is found, return None
return None

def list_names(self, name: str = '', recursive: bool = False) -> Optional[List[str]]:
"""
:param name: The command name in the namespace. Defaults to the root.
:param recursive: Whether to list all descendant names or only child names.
:return: A list of all names under the given name, or None if the name is invalid.
"""
if not self.is_valid_name(name):
return None
commands = set()
path = os.path.join(*name.split('.'))
found = False
for branch in self.branches:
full_path = os.path.join(self.root_path, branch, path)
if os.path.exists(full_path):
found = True
if os.path.isdir(full_path):
self._add_dirnames_to_commands(full_path, name, commands)
if recursive:
self._add_recursive_dirnames_to_commands(full_path, name, commands)
return sorted(commands) if found else None

def _add_dirnames_to_commands(self, full_path: str, name: str, commands: set):
for dirname in os.listdir(full_path):
command_name = '.'.join([name, dirname]) if name else dirname
commands.add(command_name)

def _add_recursive_dirnames_to_commands(self, full_path: str, name: str, commands: set):
for dirpath, dirnames, _ in os.walk(full_path):
for dirname in dirnames:
relative_path = os.path.relpath(dirpath, full_path).replace(os.sep, '.')
if relative_path != '.':
command_name = ('.'.join([name, relative_path, dirname])
if name else '.'.join([relative_path, dirname]))
else:
command_name = '.'.join([name, dirname]) if name else dirname
commands.add(command_name)
55 changes: 55 additions & 0 deletions tests/test_namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,33 @@
from devchat.namespace import Namespace


def test_is_valid_name():
# Test valid names
assert Namespace.is_valid_name('') is True
assert Namespace.is_valid_name('a') is True
assert Namespace.is_valid_name('A.b') is True
assert Namespace.is_valid_name('a.2.c') is True
assert Namespace.is_valid_name('a_b') is True
assert Namespace.is_valid_name('a-b') is True
assert Namespace.is_valid_name('a_3.4-d') is True

# Test invalid names
assert Namespace.is_valid_name('.') is False
assert Namespace.is_valid_name('..') is False
assert Namespace.is_valid_name('a..b') is False
assert Namespace.is_valid_name('.a') is False
assert Namespace.is_valid_name('3.') is False
assert Namespace.is_valid_name('a/.b') is False
assert Namespace.is_valid_name('a\\b') is False
assert Namespace.is_valid_name('a*b') is False
assert Namespace.is_valid_name('a?1') is False
assert Namespace.is_valid_name('a:b') is False
assert Namespace.is_valid_name('a|b') is False
assert Namespace.is_valid_name('a"b') is False
assert Namespace.is_valid_name('2<b') is False
assert Namespace.is_valid_name('a>b') is False


def test_get_path(tmp_path):
# Create a Namespace instance with the temporary directory as the root path
namespace = Namespace(str(tmp_path))
Expand All @@ -24,3 +51,31 @@ def test_get_path(tmp_path):
os.makedirs(os.path.join(tmp_path, 'usr', 'j', 'k', 'l'), exist_ok=True)
os.makedirs(os.path.join(tmp_path, 'sys', 'j', 'k', 'l'), exist_ok=True)
assert namespace.get_path('j.k.l') == os.path.join('usr', 'j', 'k', 'l')


def test_list_names(tmp_path):
os.makedirs(os.path.join(tmp_path, 'usr', 'a', 'b', 'c'))
os.makedirs(os.path.join(tmp_path, 'org', 'a', 'b', 'd'))
os.makedirs(os.path.join(tmp_path, 'sys', 'a', 'e'))

namespace = Namespace(str(tmp_path))

# Test listing child commands
commands = namespace.list_names('a')
assert commands == ['a.b', 'a.e']

# Test listing all descendant commands
commands = namespace.list_names('a', recursive=True)
assert commands == ['a.b', 'a.b.c', 'a.b.d', 'a.e']

# Test listing commands of an invalid name
commands = namespace.list_names('b')
assert commands is None

# Test listing commands when there are no commands
commands = namespace.list_names('a.e')
assert not commands

# Test listing commands of the root
commands = namespace.list_names()
assert commands == ['a']

0 comments on commit 579398b

Please sign in to comment.