Skip to content

Commit 3edc3e5

Browse files
committed
Revised the code for second pass of PR.
1 parent 7236111 commit 3edc3e5

File tree

9 files changed

+67
-102
lines changed

9 files changed

+67
-102
lines changed

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,9 @@ with each later location overriding any earlier options.
162162
Here are the places options can be specified in the order that they are checked:
163163

164164
1. Global Configuration --
165-
`<platform-specific user config location>/autograder.json` which is considered the "proper" place to store user-related config for the platform you are using (according to [platformdirs](https://github.com/platformdirs/platformdirs)). Use `--help` to see the exact place in your specific case. This is a great place to store login credentials.
165+
`<platform-specific user config location>/autograder.json` which is considered the "proper" place to store user-related config
166+
for the platform you are using (according to [platformdirs](https://github.com/platformdirs/platformdirs)).
167+
Use `--help` to see the exact place in your specific case. This is a great place to store login credentials.
166168

167169
2. Local Configuration --
168170
Local configuration files can be found in different locations, the first file found will be used and no other locations are searched.
@@ -179,7 +181,7 @@ Here are the places options can be specified in the order that they are checked:
179181

180182
A base config file (`autograder.json`) is often distributed with assignments that contains most the settings you need.
181183
You can modify this config to include your settings and use that for setting all your configuration options.
182-
A `autograder.json` file may look something like:
184+
An `autograder.json` file may look something like:
183185
```json
184186
{
185187
"course": "my-course",

autograder/api/config.py

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
DEFAULT_GLOBAL_CONFIG_PATH = platformdirs.user_config_dir('autograder.json')
1616
CSV_TO_LIST_DELIMITER = ','
1717
MAP_KEY_VALUE_SEP = '='
18+
DEPTH_LIMIT = 10000
19+
CONFIG_TYPE_DELIMITER = "::"
20+
1821

1922
class APIParam(object):
2023
def __init__(self, key, description,
@@ -99,36 +102,37 @@ def _parse_api_config(config, params, additional_required_keys, additional_optio
99102

100103
return data, extra
101104

102-
# The local_config_search_limit input limits config search depth.
103-
# This helps to prevent detection of autograder.json files in higher directories during testing.
104-
def get_local_config_path(local_config_search_limit = None):
105+
def get_local_config_path(local_config_root_cutoff = None):
105106
"""
106-
Searches for a configuration file in a hierarchical order, starting with ./autograder.json,
107-
then ./config.json, and continuing up the directory tree looking for autograder.json.
107+
Searches for a configuration file in a hierarchical order,
108+
starting with DEFAULT_CONFIG_FILENAME, then LEGACY_CONFIG_FILENAME,
109+
and continuing up the directory tree looking for DEFAULT_CONFIG_FILENAME.
108110
Returns the path to the first valid configuration file found.
109-
If no configuration file is found, returns None.
111+
112+
If no configuration file is found, returns None. The cutoff limits config search depth.
113+
This helps to prevent detection of DEFAULT_CONFIG_FILENAME files in higher directories during testing.
110114
"""
111115

112-
# autograder.json file in current directory
116+
# The case where DEFAULT_CONFIG_FILENAME file in current directory.
113117
if (os.path.isfile(DEFAULT_CONFIG_FILENAME)):
114118
return os.path.abspath(DEFAULT_CONFIG_FILENAME)
115119

116-
# config.json file in current directory
120+
# The case where LEGACY_CONFIG_FILENAME file in current directory.
117121
if (os.path.isfile(LEGACY_CONFIG_FILENAME)):
118122
return os.path.abspath(LEGACY_CONFIG_FILENAME)
119123

120-
# An autograder.json file located in any ancestor directory on the path to root.
124+
# The case where a DEFAULT_CONFIG_FILENAME file located in any ancestor directory on the path to root.
121125
parent_dir = os.path.dirname(os.getcwd())
122126
return _get_ancestor_config_file_path(
123127
parent_dir,
124-
local_config_search_limit = local_config_search_limit
128+
local_config_root_cutoff = local_config_root_cutoff
125129
)
126130

127131
def get_tiered_config(
128132
cli_arguments,
129133
skip_keys = [CONFIG_PATHS_KEY],
130134
global_config_path = DEFAULT_GLOBAL_CONFIG_PATH,
131-
local_config_search_limit = None):
135+
local_config_root_cutoff = None):
132136
"""
133137
Get all the tiered configuration options (from files and CLI).
134138
If |show_sources| is True, then an addition dict will be returned that shows each key,
@@ -146,8 +150,8 @@ def get_tiered_config(
146150
_load_configs_and_sources(global_config_path, config, sources, "<global config file>")
147151

148152
# Check the local user config file.
149-
local_config_path = get_local_config_path(local_config_search_limit = local_config_search_limit)
150-
if local_config_path is not None:
153+
local_config_path = get_local_config_path(local_config_root_cutoff = local_config_root_cutoff)
154+
if (local_config_path is not None):
151155
_load_configs_and_sources(local_config_path, config, sources, "<local config file>")
152156

153157
# Check the config file specified on the command-line.
@@ -220,38 +224,39 @@ def _load_configs_and_sources(config_path, config, sources, type_of_config):
220224
with open(config_path, 'r') as file:
221225
for key, value in json.load(file).items():
222226
config[key] = value
223-
sources[key] = "%s::%s" % (type_of_config, os.path.abspath(config_path))
227+
sources[key] = f"{type_of_config}::{os.path.abspath(config_path)}"
224228

225229
def _get_ancestor_config_file_path(
226230
current_directory,
227231
config_file = DEFAULT_CONFIG_FILENAME,
228-
local_config_search_limit = None):
232+
local_config_root_cutoff = None):
229233
"""
230-
Search through the parent directories (until root) for a configuration file.
231-
Stops at the first occurrence of the specified config file
234+
Search through the parent directories (until root or a given cutoff directory)
235+
for a configuration file. Stops at the first occurrence of the specified config file
232236
(default: DEFAULT_CONFIG_FILENAME) along the path to root.
233237
Returns the path if a configuration file is found.
234238
Otherwise, returns None.
235239
"""
236240

237-
while True:
238-
parent_dir = os.path.dirname(current_directory)
239-
config_file_path = os.path.join(current_directory, config_file)
241+
current_directory = os.path.abspath(current_directory)
240242

243+
# DEPTH_LIMIT ensures termination on filesystems where the root directory is not equal to its parent.
244+
for _ in range(DEPTH_LIMIT):
245+
config_file_path = os.path.join(current_directory, config_file)
241246
if (os.path.isfile(config_file_path)):
242247
return config_file_path
243248

244-
if local_config_search_limit == current_directory:
249+
if (local_config_root_cutoff == current_directory):
245250
break
246251

247-
if parent_dir == current_directory:
252+
parent_dir = os.path.dirname(current_directory)
253+
if (parent_dir == current_directory):
248254
break
249255

250-
current_directory = os.path.dirname(current_directory)
256+
current_directory = parent_dir
251257

252258
return None
253259

254-
255260
def _submission_add_func(parser, param):
256261
parser.add_argument('submissions', metavar = 'SUBMISSION',
257262
action = 'store', type = str, nargs = '+',

autograder/cli/config/list.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,21 @@
55
DESCRIPTION = "List your current configuration options."
66

77
def run(args):
8-
arguments = {
9-
"cli_arguments": args,
10-
"skip_keys": [
8+
config, sources = autograder.api.config.get_tiered_config(
9+
cli_arguments = args,
10+
skip_keys = [
1111
'show_origin', 'verbose',
1212
autograder.api.config.CONFIG_PATHS_KEY, 'global_config_path',
1313
],
14-
"global_config_path": args.global_config_path
15-
}
14+
global_config_path = args.global_config_path
15+
)
1616

1717
config_list = []
18-
config, sources = autograder.api.config.get_tiered_config(**arguments)
19-
for key, value in config.items():
18+
for (key, value) in config.items():
2019
config_str = ''
2120
if args.show_origin:
2221
raw_source = sources.get(key)
23-
source_path = raw_source.split("::")[1]
22+
source_path = raw_source.split(autograder.api.config.CONFIG_TYPE_DELIMITER)[1]
2423
config_str = source_path + "\t"
2524

2625
config_str += "%s: %s" % (key, value)
@@ -35,11 +34,11 @@ def _get_parser():
3534
skip_server = True)
3635

3736
parser.add_argument("--show-origin", dest = 'show_origin',
38-
action = 'store_true', help = 'Show origin of configs.')
37+
action = 'store_true', help = "Shows where each configuration's value was obtained from.")
3938

4039
parser.add_argument("--global-config", dest = 'global_config_path',
4140
action = 'store', type = str, default = autograder.api.config.DEFAULT_GLOBAL_CONFIG_PATH,
42-
help = 'Path to the global configuration file. (default: %(default)s)')
41+
help = 'Path to the global configuration file (default: %(default)s).')
4342

4443
return parser
4544

autograder/util/dirent.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def get_temp_path(prefix = '', suffix = '', rm = True):
1818
if (rm):
1919
atexit.register(remove, path)
2020

21-
return os.path.realpath(path)
21+
return path
2222

2323
def remove(path):
2424
if (not os.path.exists(path)):

tests/base.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ class BaseTest(unittest.TestCase):
88
A base class for tests.
99
"""
1010

11+
maxDiff = None
12+
1113
def assertListEqual(self, a, b):
1214
a_json = json.dumps(a, indent = 4)
1315
b_json = json.dumps(b, indent = 4)

tests/cli/test_cli.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
TEST_CASE_SEP = '---'
2020
TEMP_DIR_ID = '__TEMP_DIR__'
21+
ABS_DATA_DIR_ID = '__ABS_DATA_DIR__'
2122

2223
DEFAULT_OUTPUT_CHECK = 'content_equals'
2324

@@ -107,8 +108,9 @@ def _load_allowed_arg_keys(self, module_name):
107108

108109
def _prepare_string(text, temp_dir):
109110
replacements = [
110-
(tests.server.base.DATA_DIR_ID, os.path.abspath(DATA_DIR)),
111+
(tests.server.base.DATA_DIR_ID, DATA_DIR),
111112
(TEMP_DIR_ID, temp_dir),
113+
(ABS_DATA_DIR_ID, os.path.abspath(DATA_DIR))
112114
]
113115

114116
for (key, base_dir) in replacements:

tests/cli/testdata/config/config_list_sources.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@
77
]
88
}
99
---
10-
__DATA_DIR__(configs/simple/autograder.json) user: user@test.edulinq.org
10+
__ABS_DATA_DIR__(configs/simple/autograder.json) user: user@test.edulinq.org

tests/server/base.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ class ServerBaseTest(tests.base.BaseTest):
1515
A base tests that need to call the mock server.
1616
"""
1717

18-
maxDiff = None
19-
2018
_server_process = None
2119
_port = None
2220

0 commit comments

Comments
 (0)