Skip to content

Commit e31c391

Browse files
ajanitshimangaPouyanpi
authored andcommitted
feat: railsignore added for config loading of LLMRails
1 parent 52a74af commit e31c391

File tree

6 files changed

+222
-1
lines changed

6 files changed

+222
-1
lines changed

.railsignore

Whitespace-only changes.

nemoguardrails/rails/llm/config.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
# limitations under the License.
1515

1616
"""Module for the configuration of rails."""
17-
17+
import fnmatch
1818
import logging
1919
import os
2020
import warnings
@@ -28,6 +28,7 @@
2828
from nemoguardrails.colang.v2_x.lang.colang_ast import Flow
2929
from nemoguardrails.colang.v2_x.lang.utils import format_colang_parsing_error_message
3030
from nemoguardrails.colang.v2_x.runtime.errors import ColangParsingError
31+
from nemoguardrails.utils import get_railsignore_patterns
3132

3233
log = logging.getLogger(__name__)
3334

@@ -556,6 +557,12 @@ def _load_path(
556557
# Followlinks to traverse symlinks instead of ignoring them.
557558

558559
for file in files:
560+
# Verify railsignore to skip loading
561+
ignored_by_railsignore = _is_file_ignored_by_railsignore(file)
562+
563+
if ignored_by_railsignore:
564+
continue
565+
559566
# This is the raw configuration that will be loaded from the file.
560567
_raw_config = {}
561568

@@ -1203,3 +1210,19 @@ def _generate_rails_flows(flows):
12031210
flow_definitions.insert(1, _LIBRARY_IMPORT + _NEWLINE * 2)
12041211

12051212
return flow_definitions
1213+
1214+
1215+
def _is_file_ignored_by_railsignore(filename: str) -> bool:
1216+
# Default no skip
1217+
should_skip_file = False
1218+
1219+
# Load candidate patterns from railsignore
1220+
candidate_patterns = get_railsignore_patterns()
1221+
1222+
# Ignore colang, kb, python modules if specified in valid railsignore glob format
1223+
if filename.endswith(".py") or filename.endswith(".co") or filename.endswith(".kb"):
1224+
for pattern in candidate_patterns:
1225+
if fnmatch.fnmatch(filename, pattern):
1226+
should_skip_file = True
1227+
1228+
return should_skip_file

nemoguardrails/utils.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from collections import namedtuple
2424
from datetime import datetime, timezone
2525
from enum import Enum
26+
from pathlib import Path
2627
from typing import Any, Dict, Tuple
2728

2829
import yaml
@@ -312,3 +313,52 @@ def snake_to_camelcase(name: str) -> str:
312313
str: The converted CamelCase string.
313314
"""
314315
return "".join(n.capitalize() for n in name.split("_"))
316+
317+
318+
def get_railsignore_path() -> Path:
319+
"""Helper to get railsignore path.
320+
321+
Returns:
322+
Path: The.railsignore file path.
323+
"""
324+
current_path = Path(__file__).resolve()
325+
326+
# Navigate to the root directory by going up 4 levels
327+
root_dir = current_path.parents[1]
328+
329+
file_path = root_dir / ".railsignore"
330+
331+
return file_path
332+
333+
334+
def get_railsignore_patterns() -> set[str]:
335+
"""
336+
Helper to retrieve all specified patterns in railsignore.
337+
Returns:
338+
Set[str]: The set of filenames or glob patterns in railsignore
339+
"""
340+
ignored_patterns = set()
341+
342+
railsignore_path = get_railsignore_path()
343+
344+
# File doesn't exist or is empty
345+
if not railsignore_path.exists() or not os.path.getsize(railsignore_path):
346+
return ignored_patterns
347+
348+
try:
349+
with open(railsignore_path, "r") as f:
350+
railsignore_entries = f.readlines()
351+
352+
# Remove comments and empty lines, and strip out any extra spaces/newlines
353+
railsignore_entries = [
354+
line.strip()
355+
for line in railsignore_entries
356+
if line.strip() and not line.startswith("#")
357+
]
358+
359+
ignored_patterns.update(railsignore_entries)
360+
return ignored_patterns
361+
362+
except FileNotFoundError:
363+
print(f"No {railsignore_path} found in the current directory.")
364+
return ignored_patterns
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
define user express greeting
2+
"hey"
3+
"hei"
4+
5+
define flow
6+
user express greeting
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
define user express greeting
2+
"hi"
3+
"hello"
4+
5+
define flow
6+
user express greeting
7+
bot express greeting

tests/test_railsignore.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
import os
17+
import shutil
18+
19+
import pytest
20+
21+
from nemoguardrails import RailsConfig
22+
from nemoguardrails.utils import get_railsignore_path, get_railsignore_patterns
23+
24+
CONFIGS_FOLDER = os.path.join(os.path.dirname(__file__), ".", "test_configs")
25+
26+
27+
@pytest.fixture(scope="function")
28+
def cleanup():
29+
# Copy current rails ignore and prepare for tests
30+
railsignore_path = get_railsignore_path()
31+
32+
temp_file_path = str(railsignore_path) + "-copy"
33+
34+
# Copy the original .railsignore to a temporary file
35+
shutil.copy(railsignore_path, temp_file_path)
36+
print(f"Copied {railsignore_path} to {temp_file_path}")
37+
38+
# Clean railsignore file before
39+
cleanup_railsignore()
40+
41+
# Yield control to test
42+
yield
43+
44+
# Clean railsignore file before
45+
cleanup_railsignore()
46+
47+
# Restore the original .railsignore from the temporary copy
48+
shutil.copy(temp_file_path, railsignore_path)
49+
print(f"Restored {railsignore_path} from {temp_file_path}")
50+
51+
# Delete the temporary file
52+
if os.path.exists(temp_file_path):
53+
os.remove(temp_file_path)
54+
print(f"Deleted temporary file {temp_file_path}")
55+
56+
57+
def test_railsignore_config_loading(cleanup):
58+
# Setup railsignore
59+
append_railsignore("ignored_config.co")
60+
61+
# Load config
62+
config = RailsConfig.from_path(os.path.join(CONFIGS_FOLDER, "railsignore_config"))
63+
64+
config_string = str(config)
65+
# Assert .railsignore successfully ignores
66+
assert "ignored_config.co" not in config_string
67+
68+
# Other files should load successfully
69+
assert "config_to_load.co" in config_string
70+
71+
72+
def test_get_railsignore_files(cleanup):
73+
# Empty railsignore
74+
ignored_files = get_railsignore_patterns()
75+
76+
assert "ignored_module.py" not in ignored_files
77+
assert "ignored_colang.co" not in ignored_files
78+
79+
# Append files to railsignore
80+
append_railsignore("ignored_module.py")
81+
append_railsignore("ignored_colang.co")
82+
83+
# Grab ignored files
84+
ignored_files = get_railsignore_patterns()
85+
86+
# Check files exist
87+
assert "ignored_module.py" in ignored_files
88+
assert "ignored_colang.co" in ignored_files
89+
90+
# Append comment and whitespace
91+
append_railsignore("# This_is_a_comment.py")
92+
append_railsignore(" ")
93+
append_railsignore("")
94+
95+
# Grab ignored files
96+
ignored_files = get_railsignore_patterns()
97+
98+
# Comments and whitespace not retrieved
99+
assert "# This_is_a_comment.py" not in ignored_files
100+
assert " " not in ignored_files
101+
assert "" not in ignored_files
102+
103+
# Assert files still exist
104+
assert "ignored_module.py" in ignored_files
105+
assert "ignored_colang.co" in ignored_files
106+
107+
108+
def cleanup_railsignore():
109+
"""
110+
Helper for clearing a railsignore file.
111+
"""
112+
railsignore_path = get_railsignore_path()
113+
114+
try:
115+
with open(railsignore_path, "w") as f:
116+
pass
117+
except OSError as e:
118+
print(f"Error: Unable to create {railsignore_path}. {e}")
119+
else:
120+
print(f"Successfully cleaned up .railsignore: {railsignore_path}")
121+
122+
123+
def append_railsignore(file_name: str) -> None:
124+
"""
125+
Helper for appending to a railsignore file.
126+
"""
127+
railsignore_path = get_railsignore_path()
128+
129+
try:
130+
with open(railsignore_path, "a") as f:
131+
f.write(file_name + "\n")
132+
except FileNotFoundError:
133+
print(f"No {railsignore_path} found in the current directory.")
134+
except OSError as e:
135+
print(f"Error: Failed to write to {railsignore_path}. {e}")

0 commit comments

Comments
 (0)