class PathExpansionTest(unittest.TestCase): def setUp(self): self.env = { "HOME": "/users/joebloggs", "SHOT": "/metropolis/shot01", "DEPT": "texturing", } def test_posix_tilde_input(self): with mock.patch.dict("os.environ", self.env): self.p = Path("~/a/b/c") self.assertEqual(self.p.posix_path(), "/users/joebloggs/a/b/c") def test_posix_var_input(self): with mock.patch.dict("os.environ", self.env): self.p = Path("$SHOT/a/b/c") self.assertEqual(self.p.posix_path(), "/metropolis/shot01/a/b/c") def test_posix_two_var_input(self): with mock.patch.dict("os.environ", self.env): self.p = Path("$SHOT/a/b/$DEPT/c") self.assertEqual(self.p.posix_path(), "/metropolis/shot01/a/b/texturing/c") def test_windows_var_input(self): with mock.patch.dict("os.environ", self.env): self.p = Path("$HOME\\a\\b\\c") self.assertEqual(self.p.windows_path(), "\\users\\joebloggs\\a\\b\\c") self.assertEqual(self.p.posix_path(), "/users/joebloggs/a/b/c")
class SpecifyDriveLetterUse(unittest.TestCase): def test_remove_from_path(self): self.p = Path("C:\\a\\b\\c") self.assertEqual(self.p.posix_path(with_drive=False), "/a/b/c") self.assertEqual(self.p.windows_path(with_drive=False), "\\a\\b\\c") def test_remove_from_root_path(self): self.p = Path("C:\\") self.assertEqual(self.p.posix_path(with_drive=False), "/") self.assertEqual(self.p.windows_path(with_drive=False), "\\")
class RootPath(unittest.TestCase): def test_root_path(self): self.p = Path("/") self.assertEqual(self.p.posix_path(), "/") self.assertEqual(self.p.windows_path(), "\\") def test_drive_letter_root_path(self): self.p = Path("C:\\") self.assertEqual(self.p.posix_path(), "C:/") self.assertEqual(self.p.windows_path(), "C:\\")
class WindowsMixedPathTest(unittest.TestCase): def test_abs_in_posix_path_out(self): self.p = Path("\\a\\b\\c/d/e") self.assertEqual(self.p.posix_path(), "/a/b/c/d/e") def test_abs_in_windows_path_out(self): self.p = Path("\\a\\b\\c/d/e") self.assertEqual(self.p.windows_path(), "\\a\\b\\c\\d\\e") def test_letter_abs_in_posix_path_out(self): self.p = Path("C:\\a\\b\\c/d/e") self.assertEqual(self.p.posix_path(), "C:/a/b/c/d/e") def test_letter_abs_in_windows_path_out(self): self.p = Path("C:\\a\\b\\c/d/e") self.assertEqual(self.p.windows_path(), "C:\\a\\b\\c\\d\\e")
class AbsPosixPathTest(unittest.TestCase): def setUp(self): self.p = Path("/a/b/c") def test_posix_path_out(self): self.assertEqual(self.p.posix_path(), "/a/b/c") def test_win_path_out(self): self.assertEqual(self.p.windows_path(), "\\a\\b\\c")
class PathContextExpansionTest(unittest.TestCase): def setUp(self): self.env = { "HOME": "/users/joebloggs", "SHOT": "/metropolis/shot01", "DEPT": "texturing", } self.context = { "HOME": "/users/janedoe", "FOO": "fooval", "BAR_FLY1_": "bar_fly1_val", "ROOT_DIR": "/some/root", } def test_path_replaces_context(self): self.p = Path("$ROOT_DIR/thefile.jpg", context=self.context) self.assertEqual(self.p.posix_path(), "/some/root/thefile.jpg") def test_path_replaces_multiple_context(self): self.p = Path("$ROOT_DIR/$BAR_FLY1_/thefile.jpg", context=self.context) self.assertEqual(self.p.posix_path(), "/some/root/bar_fly1_val/thefile.jpg") def test_path_context_overrides_env(self): self.p = Path("$HOME/thefile.jpg", context=self.context) self.assertEqual(self.p.posix_path(), "/users/janedoe/thefile.jpg") def test_path_leave_unknown_variable_in_tact(self): self.p = Path("$ROOT_DIR/$BAR_FLY1_/$FOO/thefile.$F.jpg", context=self.context) self.assertEqual(self.p.posix_path(), "/some/root/bar_fly1_val/fooval/thefile.$F.jpg") def test_relative_path_var_fails(self): with self.assertRaises(ValueError): self.p = Path("$FOO/a/b/c", context=self.context)
class AbsWindowsPathTest(unittest.TestCase): def setUp(self): self.p = Path("C:\\a\\b\\c") def test_posix_path_out(self): self.assertEqual(self.p.posix_path(), "C:/a/b/c") def test_win_path_out(self): self.assertEqual(self.p.windows_path(), "C:\\a\\b\\c") # consider just testing on both platforms def test_os_path_out(self): with mock.patch("os.name", "posix"): self.assertEqual(self.p.os_path(), "C:/a/b/c") with mock.patch("os.name", "nt"): self.assertEqual(self.p.os_path(), "C:\\a\\b\\c")
class Submission(object): """ Submission holds all data needed for a submission. It has potentially many Jobs, and those Jobs each have many Tasks. A Submission can provide the correct args to send to Conductor, or it can be used to create a dry run to show the user what will happen. A Submission also sets a list of tokens that the user can access as <angle bracket> tokens in order to build strings in the UI such as commands, job title, and (soon to be added) metadata. """ def __init__(self, obj): """ Collect data from the Clarisse UI. Collect attribute values that are common to all jobs, then call _set_tokens(). After _set_tokens has been called, the Submission level token variables are valid and calls to evaluate expressions will correctly resolve where those tokens have been used. """ self.node = obj if self.node.is_kindof("ConductorJob"): self.nodes = [obj] else: raise NotImplementedError self.localize_before_ship = self.node.get_attribute( "localize_contexts").get_bool() self.project_filename = ix.application.get_current_project_filename() self.timestamp = datetime.datetime.now().strftime("%Y_%m_%d_%H_%M_%S") self.timestamp_render_package = self.node.get_attribute( "timestamp_render_package").get_bool() self.tmpdir = Path( os.path.join( ix.application.get_factory().get_vars().get( "CTEMP").get_string(), "conductor", )) self.render_package_path = self._get_render_package_path() self.should_delete_render_package = self.node.get_attribute( "clean_up_render_package").get_bool() self.local_upload = self.node.get_attribute("local_upload").get_bool() self.force_upload = self.node.get_attribute("force_upload").get_bool() self.upload_only = self.node.get_attribute("upload_only").get_bool() self.project = self._get_project() self.notifications = self._get_notifications() self.tokens = self._set_tokens() self.jobs = [] for node in self.nodes: job = Job(node, self.tokens, self.render_package_path) self.jobs.append(job) def _get_project(self): """Get the project from the attr. Get its ID in case the current project is no longer in the list of projects at conductor, throw an error. """ projects = ConductorDataBlock().projects() project_att = self.node.get_attribute("conductor_project_name") label = project_att.get_applied_preset_label() try: found = next(p for p in projects if str(p["name"]) == label) except StopIteration: ix.log_error( 'Cannot find project "{}" at Conductor.'.format(label)) return {"id": found["id"], "name": str(found["name"])} def _get_notifications(self): """Get notification prefs.""" if not self.node.get_attribute("notify").get_bool(): return None emails = self.node.get_attribute("email_addresses").get_string() return [email.strip() for email in emails.split(",") if email.strip()] def _set_tokens(self): """Env tokens are variables to help the user build expressions. The user interface has fields for strings such as job title, task command. The user can use these tokens with <angle brackets> to build those strings. Tokens at the Submission level are also available in Job level fields, and likewise tokens at the Job level are available in Task level fields. """ tokens = {} pdir_val = ix.application.get_factory().get_vars().get( "PDIR").get_string() tokens["ct_pdir"] = '"{}"'.format( Path(pdir_val).posix_path(with_drive=False)) tokens["ct_temp_dir"] = "{}".format( self.tmpdir.posix_path(with_drive=False)) tokens["ct_timestamp"] = self.timestamp tokens["ct_submitter"] = self.node.get_name() tokens["ct_render_package"] = '"{}"'.format( self.render_package_path.posix_path(with_drive=False)) tokens["ct_project"] = self.project["name"] return tokens def _get_render_package_path(self): """ Calc the path to the render package. The name is not always known until preview/submission time because it is based on the filename and possibly a timestamp. What this means, practically, is that it won't show up in the extra uploads window along with other dependencies when the glob or smart-scan button is pushed. It will however always show up in the preview window. We replace spaces in the filename because of a bug in Clarisse https://www.isotropix.com/user/bugtracker/376 Returns: string: path """ current_filename = ix.application.get_current_project_filename() path = os.path.splitext(current_filename)[0] path = os.path.join(os.path.dirname(path), os.path.basename(path).replace(" ", "_")) try: if self.timestamp_render_package: return Path("{}_ct{}.project".format(path, self.timestamp)) else: return Path("{}_ct.project".format(path)) except ValueError: ix.log_error( 'Cannot create a submission from this file: "{}". Has it ever been saved?' .format(current_filename)) def get_args(self): """ Prepare the args for submission to conductor. Returns: list: list of dicts containing submission args per job. """ result = [] submission_args = {} submission_args["local_upload"] = self.local_upload submission_args["upload_only"] = self.upload_only submission_args["force"] = self.force_upload submission_args["project"] = self.project["name"] submission_args["notify"] = self.notifications for job in self.jobs: args = job.get_args(self.upload_only) args.update(submission_args) result.append(args) return result def submit(self): """ Submit all jobs. Returns: list: list of response dictionaries, containing response codes and descriptions. """ submission_args = self.get_args() self.write_render_package() do_submission, submission_args = self.legalize_upload_paths( submission_args) results = [] if do_submission: for job_args in submission_args: try: remote_job = conductor_submit.Submit(job_args) response, response_code = remote_job.main() results.append({ "code": response_code, "response": response }) except BaseException: results.append({ "code": "undefined", "response": "".join(traceback.format_exception(*sys.exc_info())), }) for result in results: ix.log_info(result) else: return [{ "code": "undefined", "response": "Submission cancelled by user" }] self._after_submit() return results def write_render_package(self): """ Write a package suitable for rendering. A render package is a project file with a special name. """ app = ix.application clarisse_window = app.get_event_window() self._before_write_package() current_filename = app.get_current_project_filename() current_window_title = clarisse_window.get_title() package_file = self.render_package_path.posix_path() with cu.disabled_app(): success = ix.application.save_project(package_file) ix.application.set_current_project_filename(current_filename) clarisse_window.set_title(current_window_title) self._after_write_package() if not success: ix.log_error( "Failed to export render package {}".format(package_file)) ix.log_info("Wrote package to {}".format(package_file)) return package_file def legalize_upload_paths(self, submission_args): """ Alert the user of missing files. If the user doesn't want to continue with missing files, the result will be False. Otherwise it will be True and the potentially adjusted args are returned. Args: submission_args (list): list of job args. Returns: tuple (bool, adjusted args): """ missing_files = [] for job_args in submission_args: existing_files = [] for path in job_args["upload_paths"]: if os.path.exists(path): existing_files.append(path) else: missing_files.append(path) job_args["upload_paths"] = existing_files missing_files = sorted(list(set(missing_files))) if not missing_files_ui.proceed(missing_files): return (False, []) return (True, submission_args) def _before_write_package(self): """ Prepare to write render package. """ if self.localize_before_ship: _localize_contexts() _remove_conductor() self._prepare_temp_directory() self._copy_system_dependencies_to_temp() def _prepare_temp_directory(self): """ Make sure the temp directory has a conductor subdirectory. """ tmpdir = self.tmpdir.posix_path() try: os.makedirs(tmpdir) except OSError as ex: if not (ex.errno == errno.EEXIST and os.path.isdir(tmpdir)): raise def _copy_system_dependencies_to_temp(self): """ Copy over all system dependencies to a tmp folder. Wrapper scripts, config files etc. The clarisse.cfg file is special. See ../clarisse_config.py """ for entry in deps.system_dependencies(): if os.path.isfile(entry["src"]): if entry["src"].endswith(".cfg"): safe_config = ccfg.legalize(entry["src"]) with open(entry["dest"], "w") as dest: dest.write(safe_config) ix.log_info("Copy with mods {} to {}".format( entry["src"], entry["dest"])) else: ix.log_info("Copy {} to {}".format(entry["src"], entry["dest"])) shutil.copy(entry["src"], entry["dest"]) def _after_submit(self): """Clean up, and potentially other post submission actions.""" self._delete_render_package() def _after_write_package(self): """ Runs operations after saving the render package. If we did something destructive, like localize contexts, then a backup will have been saved and we now reload it. This strategy is used because Clarisse's undo is broken when it comes to undoing context localization. """ if self.localize_before_ship: self._revert_to_saved_scene() def _delete_render_package(self): """ Delete the render package from disk if the user wants to. """ if self.should_delete_render_package: render_package_file = self.render_package_path.posix_path() if os.path.exists(render_package_file): os.remove(render_package_file) def _revert_to_saved_scene(self): """ If contexts were localized, we will load the saved scene. """ with cu.waiting_cursor(): with cu.disabled_app(): ix.application.load_project(self.project_filename) @property def node_name(self): """node_name.""" return self.node.get_name() @property def filename(self): """filename.""" return ix.application.get_current_project_filename() def has_notifications(self): """has_notifications.""" return bool(self.notifications) @property def email_addresses(self): """email_addresses.""" if not self.has_notifications(): return [] return self.notifications["email"]["addresses"]
def test_path_collapses_to_root(self): p = Path("/a/b/../../") self.assertEqual(p.posix_path(), "/") self.assertEqual(p.depth, 0)
def test_path_collapses_many_non_consecutive_mixed_dots(self): p = Path("/a/./b/c/../.././d/../././e/f/../g/./") self.assertEqual(p.posix_path(), "/a/e/g") self.assertEqual(p.depth, 3)
def test_path_collapses_many_non_consecutive_double_dots(self): p = Path("/a/b/c/../../d/../e/f/../g") self.assertEqual(p.posix_path(), "/a/e/g")
def test_path_collapses_many_single_dots(self): p = Path("/a/b/./c/././d") self.assertEqual(p.posix_path(), "/a/b/c/d")
def test_path_collapses_double_dot(self): p = Path("/a/b/../c") self.assertEqual(p.posix_path(), "/a/c")