Robot Framework
namespace.py
Go to the documentation of this file.
1 # Copyright 2008-2015 Nokia Networks
2 # Copyright 2016- Robot Framework Foundation
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 copy
17 import os
18 from collections import OrderedDict
19 from itertools import chain
20 
21 from robot.errors import DataError, KeywordError
22 from robot.libraries import STDLIBS
23 from robot.output import LOGGER, Message
24 from robot.utils import (RecommendationFinder, eq, find_file, is_string, normalize,
25  printable_name, seq2str2)
26 
27 from .context import EXECUTION_CONTEXTS
28 from .importer import ImportCache, Importer
29 from .model import Import
30 from .runkwregister import RUN_KW_REGISTER
31 from .usererrorhandler import UserErrorHandler
32 from .userkeyword import UserLibrary
33 
34 IMPORTER = Importer()
35 
36 
37 class Namespace:
38 
41  _default_libraries = ('BuiltIn', 'Reserved', 'Easter')
42 
45  _library_import_by_path_ends = ('.py', '/', os.sep)
46 
49  _variables_import_by_path_ends = _library_import_by_path_ends + ('.yaml', '.yml')
50 
51  def __init__(self, variables, suite, resource, languages):
52  LOGGER.info(f"Initializing namespace for suite '{suite.longname}'.")
53  self.variablesvariables = variables
54  self.languageslanguages = languages
55  self._imports_imports = resource.imports
56  self._kw_store_kw_store = KeywordStore(resource, languages)
57  self._imported_variable_files_imported_variable_files = ImportCache()
58  self._suite_name_suite_name = suite.longname
59  self._running_test_running_test = False
60 
61  @property
62  libraries = property
63 
64  def libraries(self):
65  return self._kw_store_kw_store.libraries.values()
66 
67  def handle_imports(self):
68  self._import_default_libraries_import_default_libraries()
69  self._handle_imports_handle_imports(self._imports_imports)
70 
72  for name in self._default_libraries_default_libraries:
73  self.import_libraryimport_library(name, notify=name == 'BuiltIn')
74 
75  def _handle_imports(self, import_settings):
76  for item in import_settings:
77  try:
78  if not item.name:
79  raise DataError(f'{item.type} setting requires value.')
80  self._import_import(item)
81  except DataError as err:
82  item.report_invalid_syntax(err.message)
83 
84  def _import(self, import_setting):
85  action = {'Library': self._import_library_import_library,
86  'Resource': self._import_resource_import_resource,
87  'Variables': self._import_variables_import_variables}[import_setting.type]
88  action(import_setting)
89 
90  def import_resource(self, name, overwrite=True):
91  self._import_resource_import_resource(Import('Resource', name), overwrite=overwrite)
92 
93  def _import_resource(self, import_setting, overwrite=False):
94  path = self._resolve_name_resolve_name(import_setting)
95  self._validate_not_importing_init_file_validate_not_importing_init_file(path)
96  if overwrite or path not in self._kw_store_kw_store.resources:
97  resource = IMPORTER.import_resource(path, self.languageslanguages)
98  self.variablesvariables.set_from_variable_table(resource.variables, overwrite)
99  user_library = UserLibrary(resource)
100  self._kw_store_kw_store.resources[path] = user_library
101  self._handle_imports_handle_imports(resource.imports)
102  LOGGER.imported("Resource", user_library.name,
103  importer=import_setting.source,
104  source=path)
105  else:
106  LOGGER.info(f"Resource file '{path}' already imported by "
107  f"suite '{self._suite_name}'.")
108 
110  name = os.path.splitext(os.path.basename(path))[0]
111  if name.lower() == '__init__':
112  raise DataError(f"Initialization file '{path}' cannot be imported as "
113  f"a resource file.")
114 
115  def import_variables(self, name, args, overwrite=False):
116  self._import_variables_import_variables(Import('Variables', name, args), overwrite)
117 
118  def _import_variables(self, import_setting, overwrite=False):
119  path = self._resolve_name_resolve_name(import_setting)
120  args = self._resolve_args_resolve_args(import_setting)
121  if overwrite or (path, args) not in self._imported_variable_files_imported_variable_files:
122  self._imported_variable_files_imported_variable_files.add((path, args))
123  self.variablesvariables.set_from_file(path, args, overwrite)
124  LOGGER.imported("Variables", os.path.basename(path),
125  args=list(args),
126  importer=import_setting.source,
127  source=path)
128  else:
129  msg = f"Variable file '{path}'"
130  if args:
131  msg += f" with arguments {seq2str2(args)}"
132  LOGGER.info(f"{msg} already imported by suite '{self._suite_name}'.")
133 
134  def import_library(self, name, args=(), alias=None, notify=True):
135  self._import_library_import_library(Import('Library', name, args, alias),
136  notify=notify)
137 
138  def _import_library(self, import_setting, notify=True):
139  name = self._resolve_name_resolve_name(import_setting)
140  lib = IMPORTER.import_library(name, import_setting.args,
141  import_setting.alias, self.variablesvariables)
142  if lib.name in self._kw_store_kw_store.libraries:
143  LOGGER.info(f"Library '{lib.name}' already imported by suite "
144  f"'{self._suite_name}'.")
145  return
146  if notify:
147  LOGGER.imported("Library", lib.name,
148  args=list(import_setting.args),
149  originalname=lib.orig_name,
150  importer=import_setting.source,
151  source=lib.source)
152  self._kw_store_kw_store.libraries[lib.name] = lib
153  lib.start_suite()
154  if self._running_test_running_test:
155  lib.start_test()
156 
157  def _resolve_name(self, setting):
158  name = setting.name
159  try:
160  name = self.variablesvariables.replace_string(name)
161  except DataError as err:
162  self._raise_replacing_vars_failed_raise_replacing_vars_failed(setting, err)
163  if self._is_import_by_path_is_import_by_path(setting.type, name):
164  return find_file(name, setting.directory, file_type=setting.type)
165  return name
166 
167  def _raise_replacing_vars_failed(self, setting, error):
168  raise DataError(f"Replacing variables from setting '{setting.type}' "
169  f"failed: {error}")
170 
171  def _is_import_by_path(self, import_type, path):
172  if import_type == 'Library':
173  return path.lower().endswith(self._library_import_by_path_ends_library_import_by_path_ends)
174  if import_type == 'Variables':
175  return path.lower().endswith(self._variables_import_by_path_ends_variables_import_by_path_ends)
176  return True
177 
178  def _resolve_args(self, import_setting):
179  try:
180  return self.variablesvariables.replace_list(import_setting.args)
181  except DataError as err:
182  self._raise_replacing_vars_failed_raise_replacing_vars_failed(import_setting, err)
183 
184  def set_search_order(self, new_order):
185  old_order = self._kw_store_kw_store.search_order
186  self._kw_store_kw_store.search_order = new_order
187  return old_order
188 
189  def start_test(self):
190  self._running_test_running_test = True
191  self.variablesvariables.start_test()
192  for lib in self.librarieslibrarieslibraries:
193  lib.start_test()
194 
195  def end_test(self):
196  self.variablesvariables.end_test()
197  for lib in self.librarieslibrarieslibraries:
198  lib.end_test()
199  self._running_test_running_test = True
200 
201  def start_suite(self):
202  self.variablesvariables.start_suite()
203 
204  def end_suite(self, suite):
205  for lib in self.librarieslibrarieslibraries:
206  lib.end_suite()
207  if not suite.parent:
208  IMPORTER.close_global_library_listeners()
209  self.variablesvariables.end_suite()
210 
212  self.variablesvariables.start_keyword()
213 
214  def end_user_keyword(self):
215  self.variablesvariables.end_keyword()
216 
217  def get_library_instance(self, libname):
218  return self._kw_store_kw_store.get_library(libname).get_instance()
219 
221  return dict((name, lib.get_instance())
222  for name, lib in self._kw_store_kw_store.libraries.items())
223 
224  def reload_library(self, libname_or_instance):
225  library = self._kw_store_kw_store.get_library(libname_or_instance)
226  library.reload()
227  return library
228 
229  def get_runner(self, name, recommend_on_failure=True):
230  try:
231  return self._kw_store_kw_store.get_runner(name, recommend_on_failure)
232  except DataError as error:
233  return UserErrorHandler(error, name)
234 
235 
237 
238  def __init__(self, resource, languages):
239  self.user_keywordsuser_keywords = UserLibrary(resource, resource_file=False)
240  self.librarieslibraries = OrderedDict()
241  self.resourcesresources = ImportCache()
242  self.search_ordersearch_order = ()
243  self.languageslanguages = languages
244 
245  def get_library(self, name_or_instance):
246  if name_or_instance is None:
247  raise DataError("Library can not be None.")
248  if is_string(name_or_instance):
249  return self._get_lib_by_name_get_lib_by_name(name_or_instance)
250  return self._get_lib_by_instance_get_lib_by_instance(name_or_instance)
251 
252  def _get_lib_by_name(self, name):
253  if name in self.librarieslibraries:
254  return self.librarieslibraries[name]
255  matches = [lib for lib in self.librarieslibraries.values() if eq(lib.name, name)]
256  if len(matches) == 1:
257  return matches[0]
258  self._no_library_found_no_library_found(name, multiple=bool(matches))
259 
260  def _no_library_found(self, name, multiple=False):
261  if multiple:
262  raise DataError(f"Multiple libraries matching '{name}' found.")
263  raise DataError(f"No library '{name}' found.")
264 
265  def _get_lib_by_instance(self, instance):
266  for lib in self.librarieslibraries.values():
267  if lib.get_instance(create=False) is instance:
268  return lib
269  self._no_library_found_no_library_found(instance)
270 
271  def get_runner(self, name, recommend=True):
272  runner = self._get_runner_get_runner(name)
273  if runner is None:
274  self._raise_no_keyword_found_raise_no_keyword_found(name, recommend)
275  return runner
276 
277  def _raise_no_keyword_found(self, name, recommend=True):
278  if name.strip(': ').upper() == 'FOR':
279  raise KeywordError(
280  f"Support for the old FOR loop syntax has been removed. "
281  f"Replace '{name}' with 'FOR', end the loop with 'END', and "
282  f"remove escaping backslashes."
283  )
284  if name == '\\':
285  raise KeywordError(
286  "No keyword with name '\\' found. If it is used inside a for "
287  "loop, remove escaping backslashes and end the loop with 'END'."
288  )
289  message = f"No keyword with name '{name}' found."
290  if recommend:
291  finder = KeywordRecommendationFinder(self.user_keywordsuser_keywords,
292  self.librarieslibraries,
293  self.resourcesresources)
294  raise KeywordError(finder.recommend_similar_keywords(name, message))
295  else:
296  raise KeywordError(message)
297 
298  def _get_runner(self, name):
299  if not name:
300  raise DataError('Keyword name cannot be empty.', syntax=True)
301  if not is_string(name):
302  raise DataError('Keyword name must be a string.', syntax=True)
303  runner = self._get_runner_from_suite_file_get_runner_from_suite_file(name)
304  if not runner and '.' in name:
305  runner = self._get_explicit_runner_get_explicit_runner(name)
306  if not runner:
307  runner = self._get_implicit_runner_get_implicit_runner(name)
308  if not runner:
309  runner = self._get_bdd_style_runner_get_bdd_style_runner(name, self.languageslanguages.bdd_prefixes)
310  return runner
311 
312  def _get_bdd_style_runner(self, name, prefixes):
313  parts = name.split()
314  for index in range(1, len(parts)):
315  prefix = ' '.join(parts[:index]).title()
316  if prefix in prefixes:
317  runner = self._get_runner_get_runner(' '.join(parts[index:]))
318  if runner:
319  runner = copy.copy(runner)
320  runner.name = name
321  return runner
322  return None
323 
324  def _get_implicit_runner(self, name):
325  runner = self._get_runner_from_resource_files_get_runner_from_resource_files(name)
326  if not runner:
327  runner = self._get_runner_from_libraries_get_runner_from_libraries(name)
328  return runner
329 
331  if name not in self.user_keywordsuser_keywords.handlers:
332  return None
333  handlers = self.user_keywordsuser_keywords.handlers.get_handlers(name)
334  if len(handlers) > 1:
335  handlers = self._select_best_matches_select_best_matches(handlers)
336  if len(handlers) > 1:
337  self._raise_multiple_keywords_found_raise_multiple_keywords_found(handlers, name)
338  runner = handlers[0].create_runner(name, self.languageslanguages)
339  ctx = EXECUTION_CONTEXTS.current
340  caller = ctx.user_keywords[-1] if ctx.user_keywords else ctx.test
341  if caller and runner.source != caller.source:
342  if self._exists_in_resource_file_exists_in_resource_file(name, caller.source):
343  message = (
344  f"Keyword '{caller.longname}' called keyword '{name}' that exists "
345  f"both in the same resource file as the caller and in the suite "
346  f"file using that resource. The keyword in the suite file is used "
347  f"now, but this will change in Robot Framework 6.0."
348  )
349  runner.pre_run_messages += Message(message, level='WARN'),
350  return runner
351 
352  def _select_best_matches(self, handlers):
353  # "Normal" matches are considered exact and win over embedded matches.
354  normal = [hand for hand in handlers if not hand.supports_embedded_args]
355  if normal:
356  return normal
357  matches = [hand for hand in handlers
358  if not self._is_worse_match_than_others_is_worse_match_than_others(hand, handlers)]
359  return matches or handlers
360 
361  def _is_worse_match_than_others(self, candidate, alternatives):
362  for other in alternatives:
363  if (candidate is not other
364  and self._is_better_match_is_better_match(other, candidate)
365  and not self._is_better_match_is_better_match(candidate, other)):
366  return True
367  return False
368 
369  def _is_better_match(self, candidate, other):
370  # Embedded match is considered better than another if the other matches
371  # it, but it doesn't match the other.
372  return other.matches(candidate.name) and not candidate.matches(other.name)
373 
374  def _exists_in_resource_file(self, name, source):
375  for resource in self.resourcesresources.values():
376  if resource.source == source and name in resource.handlers:
377  return True
378  return False
379 
381  handlers = [handler for res in self.resourcesresources.values()
382  for handler in res.handlers_for(name)]
383  if not handlers:
384  return None
385  if len(handlers) > 1:
386  handlers = self._prioritize_same_file_or_public_prioritize_same_file_or_public(handlers)
387  if len(handlers) > 1:
388  handlers = self._select_best_matches_select_best_matches(handlers)
389  if len(handlers) > 1:
390  handlers = self._filter_based_on_search_order_filter_based_on_search_order(handlers)
391  if len(handlers) != 1:
392  self._raise_multiple_keywords_found_raise_multiple_keywords_found(handlers, name)
393  return handlers[0].create_runner(name, self.languageslanguages)
394 
395  def _get_runner_from_libraries(self, name):
396  handlers = [handler for lib in self.librarieslibraries.values()
397  for handler in lib.handlers_for(name)]
398  if not handlers:
399  return None
400  pre_run_message = None
401  if len(handlers) > 1:
402  handlers = self._select_best_matches_select_best_matches(handlers)
403  if len(handlers) > 1:
404  handlers = self._filter_based_on_search_order_filter_based_on_search_order(handlers)
405  if len(handlers) > 1:
406  handlers, pre_run_message = self._filter_stdlib_handler_filter_stdlib_handler(handlers)
407  if len(handlers) != 1:
408  self._raise_multiple_keywords_found_raise_multiple_keywords_found(handlers, name)
409  runner = handlers[0].create_runner(name, self.languageslanguages)
410  if pre_run_message:
411  runner.pre_run_messages += (pre_run_message,)
412  return runner
413 
414  def _prioritize_same_file_or_public(self, handlers):
415  user_keywords = EXECUTION_CONTEXTS.current.user_keywords
416  if user_keywords:
417  parent_source = user_keywords[-1].source
418  matches = [h for h in handlers if h.source == parent_source]
419  if matches:
420  return matches
421  matches = [handler for handler in handlers if not handler.private]
422  return matches or handlers
423 
424  def _filter_based_on_search_order(self, handlers):
425  for libname in self.search_ordersearch_order:
426  matches = [hand for hand in handlers if eq(libname, hand.libname)]
427  if matches:
428  return matches
429  return handlers
430 
431  def _filter_stdlib_handler(self, handlers):
432  warning = None
433  if len(handlers) != 2:
434  return handlers, warning
435  stdlibs_without_remote = STDLIBS - {'Remote'}
436  if handlers[0].library.orig_name in stdlibs_without_remote:
437  standard, custom = handlers
438  elif handlers[1].library.orig_name in stdlibs_without_remote:
439  custom, standard = handlers
440  else:
441  return handlers, warning
442  if not RUN_KW_REGISTER.is_run_keyword(custom.library.orig_name, custom.name):
443  warning = self._custom_and_standard_keyword_conflict_warning_custom_and_standard_keyword_conflict_warning(custom, standard)
444  return [custom], warning
445 
447  custom_with_name = standard_with_name = ''
448  if custom.library.name != custom.library.orig_name:
449  custom_with_name = f" imported as '{custom.library.name}'"
450  if standard.library.name != standard.library.orig_name:
451  standard_with_name = f" imported as '{standard.library.name}'"
452  return Message(
453  f"Keyword '{standard.name}' found both from a custom library "
454  f"'{custom.library.orig_name}'{custom_with_name} and a standard library "
455  f"'{standard.library.orig_name}'{standard_with_name}. The custom keyword "
456  f"is used. To select explicitly, and to get rid of this warning, use "
457  f"either '{custom.longname}' or '{standard.longname}'.", level='WARN'
458  )
459 
460  def _get_explicit_runner(self, name):
461  handlers_and_names = [
462  (handler, kw_name)
463  for owner_name, kw_name in self._yield_owner_and_kw_names_yield_owner_and_kw_names(name)
464  for handler in self._yield_handlers_yield_handlers(owner_name, kw_name)
465  ]
466  if not handlers_and_names:
467  return None
468  if len(handlers_and_names) == 1:
469  handler, kw_name = handlers_and_names[0]
470  else:
471  handlers = [h for h, n in handlers_and_names]
472  matches = self._select_best_matches_select_best_matches(handlers)
473  if len(matches) > 1:
474  self._raise_multiple_keywords_found_raise_multiple_keywords_found(handlers, name, implicit=False)
475  handler, kw_name = handlers_and_names[handlers.index(matches[0])]
476  return handler.create_runner(kw_name, self.languageslanguages)
477 
478  def _yield_owner_and_kw_names(self, full_name):
479  tokens = full_name.split('.')
480  for i in range(1, len(tokens)):
481  yield '.'.join(tokens[:i]), '.'.join(tokens[i:])
482 
483  def _yield_handlers(self, owner_name, name):
484  for owner in chain(self.librarieslibraries.values(), self.resourcesresources.values()):
485  if eq(owner.name, owner_name) and name in owner.handlers:
486  yield from owner.handlers.get_handlers(name)
487 
488  def _raise_multiple_keywords_found(self, handlers, name, implicit=True):
489  if any(hand.supports_embedded_args for hand in handlers):
490  error = f"Multiple keywords matching name '{name}' found"
491  else:
492  error = f"Multiple keywords with name '{name}' found"
493  if implicit:
494  error += ". Give the full name of the keyword you want to use"
495  names = sorted(hand.longname for hand in handlers)
496  raise KeywordError('\n '.join([error+':'] + names))
497 
498 
500 
501  def __init__(self, user_keywords, libraries, resources):
502  self.user_keywordsuser_keywords = user_keywords
503  self.librarieslibraries = libraries
504  self.resourcesresources = resources
505 
506 
507  def recommend_similar_keywords(self, name, message):
508  candidates = self._get_candidates_get_candidates('.' in name)
509  finder = RecommendationFinder(
510  lambda name: normalize(candidates.get(name, name), ignore='_')
511  )
512  return finder.find_and_format(name, candidates, message,
513  check_missing_argument_separator=True)
514 
515  @staticmethod
516  def format_recommendations(message, recommendations):
517  return RecommendationFinder().format(message, recommendations)
518 
519  def _get_candidates(self, use_full_name):
520  names = {}
521  for owner, name in self._get_all_handler_names_get_all_handler_names():
522  full_name = f'{owner}.{name}' if owner else name
523  names[full_name] = full_name if use_full_name else name
524  return names
525 
526 
530  handlers = [('', printable_name(handler.name, True))
531  for handler in self.user_keywordsuser_keywords.handlers]
532  for library in chain(self.librarieslibraries.values(), self.resourcesresources.values()):
533  if library.name != 'Reserved':
534  handlers.extend(
535  ((library.name or '',
536  printable_name(handler.name, code_style=True))
537  for handler in library.handlers))
538  return sorted(handlers)
Used when no keyword is found or there is more than one match.
Definition: errors.py:82
Keeps track on and optionally caches imported items.
Definition: importer.py:118
def recommend_similar_keywords(self, name, message)
Return keyword names similar to name.
Definition: namespace.py:507
def __init__(self, user_keywords, libraries, resources)
Definition: namespace.py:501
def _get_all_handler_names(self)
Return a list of (library_name, handler_name) tuples.
Definition: namespace.py:529
def format_recommendations(message, recommendations)
Definition: namespace.py:516
def _is_better_match(self, candidate, other)
Definition: namespace.py:369
def _raise_no_keyword_found(self, name, recommend=True)
Definition: namespace.py:277
def _get_lib_by_instance(self, instance)
Definition: namespace.py:265
def _get_runner_from_libraries(self, name)
Definition: namespace.py:395
def _prioritize_same_file_or_public(self, handlers)
Definition: namespace.py:414
def _yield_handlers(self, owner_name, name)
Definition: namespace.py:483
def _get_runner_from_resource_files(self, name)
Definition: namespace.py:380
def _is_worse_match_than_others(self, candidate, alternatives)
Definition: namespace.py:361
def _raise_multiple_keywords_found(self, handlers, name, implicit=True)
Definition: namespace.py:488
def _custom_and_standard_keyword_conflict_warning(self, custom, standard)
Definition: namespace.py:446
def _filter_stdlib_handler(self, handlers)
Definition: namespace.py:431
def _filter_based_on_search_order(self, handlers)
Definition: namespace.py:424
def _get_runner_from_suite_file(self, name)
Definition: namespace.py:330
def get_library(self, name_or_instance)
Definition: namespace.py:245
def _select_best_matches(self, handlers)
Definition: namespace.py:352
def _yield_owner_and_kw_names(self, full_name)
Definition: namespace.py:478
def get_runner(self, name, recommend=True)
Definition: namespace.py:271
def _no_library_found(self, name, multiple=False)
Definition: namespace.py:260
def __init__(self, resource, languages)
Definition: namespace.py:238
def _get_bdd_style_runner(self, name, prefixes)
Definition: namespace.py:312
def _exists_in_resource_file(self, name, source)
Definition: namespace.py:374
def get_runner(self, name, recommend_on_failure=True)
Definition: namespace.py:229
def _import_variables(self, import_setting, overwrite=False)
Definition: namespace.py:118
def _is_import_by_path(self, import_type, path)
Definition: namespace.py:171
def set_search_order(self, new_order)
Definition: namespace.py:184
def __init__(self, variables, suite, resource, languages)
Definition: namespace.py:51
def _raise_replacing_vars_failed(self, setting, error)
Definition: namespace.py:167
def _resolve_name(self, setting)
Definition: namespace.py:157
def _import_library(self, import_setting, notify=True)
Definition: namespace.py:138
def _import_resource(self, import_setting, overwrite=False)
Definition: namespace.py:93
def import_resource(self, name, overwrite=True)
Definition: namespace.py:90
def _handle_imports(self, import_settings)
Definition: namespace.py:75
def import_library(self, name, args=(), alias=None, notify=True)
Definition: namespace.py:134
def import_variables(self, name, args, overwrite=False)
Definition: namespace.py:115
def _import(self, import_setting)
Definition: namespace.py:84
def _resolve_args(self, import_setting)
Definition: namespace.py:178
def _validate_not_importing_init_file(self, path)
Definition: namespace.py:109
def reload_library(self, libname_or_instance)
Definition: namespace.py:224
def get_library_instance(self, libname)
Definition: namespace.py:217
Created if creating handlers fail – running raises DataError.
def eq(str1, str2, ignore=(), caseless=True, spaceless=True)
Definition: match.py:24
def printable_name(string, code_style=False)
Generates and returns printable name from the given string.
Definition: misc.py:40
def normalize(string, ignore=(), caseless=True, spaceless=True)
Normalizes given string according to given spec.
Definition: normalizing.py:27
def find_file(path, basedir='.', file_type=None)
Definition: robotpath.py:132