def test_sample_grandchild(self, tmpdir, spec_type, target, atacseq_piface_data, atac_pipe_name): """ The subtype to be used can be a grandchild of Sample. """ pipe_path = os.path.join(tmpdir.strpath, atac_pipe_name) intermediate_sample_subtype = "Middle" leaf_sample_subtype = "Leaf" intermediate_subtype_lines = _class_definition_lines( intermediate_sample_subtype, Sample.__name__) leaf_subtype_lines = _class_definition_lines( leaf_sample_subtype, intermediate_sample_subtype) with open(pipe_path, 'w') as pipe_mod_file: pipe_mod_file.write("{}\n\n".format(SAMPLE_IMPORT)) for l in intermediate_subtype_lines: pipe_mod_file.write(l) pipe_mod_file.write("\n\n") for l in leaf_subtype_lines: pipe_mod_file.write(l) atacseq_piface_data[atac_pipe_name][SUBTYPES_KEY] = \ target if spec_type == "single" else \ {ATAC_PROTOCOL_NAME: target} conf_path = _write_config_data( protomap={ATAC_PROTOCOL_NAME: atac_pipe_name}, conf_data=atacseq_piface_data, dirpath=tmpdir.strpath) piface = ProtocolInterface(conf_path) subtype = piface.fetch_sample_subtype(protocol=ATAC_PROTOCOL_NAME, strict_pipe_key=atac_pipe_name, full_pipe_path=pipe_path) assert target == subtype.__name__
def test_subtype_is_not_Sample(self, tmpdir, atac_pipe_name, subtype_name, test_type, atacseq_piface_data_with_subtypes, subtypes_section_spec_type): """ Subtype in interface but not in pipeline is exceptional. """ pipe_path = os.path.join(tmpdir.strpath, atac_pipe_name) # Write out pipeline module file with non-Sample class definition. lines = _class_definition_lines(subtype_name, name_super_type="object") with open(pipe_path, 'w') as pipe_module_file: pipe_module_file.write("{}\n\n".format(SAMPLE_IMPORT)) for l in lines: pipe_module_file.write(l) # Create the ProtocolInterface and do the test call. path_config_file = _write_config_data( protomap={ATAC_PROTOCOL_NAME: atac_pipe_name}, conf_data=atacseq_piface_data_with_subtypes, dirpath=tmpdir.strpath) piface = ProtocolInterface(path_config_file) with pytest.raises(ValueError): piface.fetch_sample_subtype(protocol=ATAC_PROTOCOL_NAME, strict_pipe_key=atac_pipe_name, full_pipe_path=pipe_path)
def test_subtypes_list(self, tmpdir, atac_pipe_name, atacseq_piface_data, spec_type): """ As singleton or within mapping, only 1 subtype allowed. """ pipe_path = os.path.join(tmpdir.strpath, atac_pipe_name) # Define the classes, writing them in the pipeline module file. subtype_names = ["ArbitraryA", "PlaceholderB"] with open(pipe_path, 'w') as pipe_module_file: pipe_module_file.write("{}\n\n".format(SAMPLE_IMPORT)) for subtype_name in subtype_names: # Have the classes be Sample subtypes. for line in _class_definition_lines( subtype_name, name_super_type=Sample.__name__): pipe_module_file.write(line) pipe_module_file.write("\n\n") # Update the ProtocolInterface data. subtype_section = subtype_names if spec_type == "single" \ else {ATAC_PROTOCOL_NAME: subtype_names} atacseq_piface_data[atac_pipe_name][SUBTYPES_KEY] = subtype_section # Create the ProtocolInterface. conf_path = _write_config_data( protomap={ATAC_PROTOCOL_NAME: atac_pipe_name}, conf_data=atacseq_piface_data, dirpath=tmpdir.strpath) piface = ProtocolInterface(conf_path) # We don't really care about exception type, just that one arises. with pytest.raises(Exception): piface.fetch_sample_subtype(protocol=ATAC_PROTOCOL_NAME, strict_pipe_key=atac_pipe_name, full_pipe_path=pipe_path)
def test_no_subtypes_section(self, tmpdir, path_config_file, atac_pipe_name, num_sample_subclasses, decoy_class): """ DEPENDS ON PIPELINE MODULE CONTENT """ # Basic values to invoke the function under test pipe_path = os.path.join(tmpdir.strpath, atac_pipe_name) piface = ProtocolInterface(path_config_file) # How to define the Sample subtypes (and non-subtype) sample_subclass_basename = "SampleSubclass" sample_lines = [ "class {basename}{index}(Sample):", "\tdef __init__(*args, **kwargs):", "\t\tsuper({basename}{index}, self).__init__(*args, **kwargs)" ] non_sample_class_lines = [ "class NonSample(object):", "\tdef __init__(self):", "\t\tsuper(NonSample, self).__init__()" ] # We expect the subtype iff there's just one Sample subtype. if num_sample_subclasses == 1: exp_subtype_name = "{}0".format(sample_subclass_basename) else: exp_subtype_name = Sample.__name__ # Fill in the class definition template lines. def populate_sample_lines(n_classes): return [[ sample_lines[0].format(basename=sample_subclass_basename, index=class_index), sample_lines[1], sample_lines[2].format(basename=sample_subclass_basename, index=class_index) ] for class_index in range(n_classes)] # Determine the groups of lines to permute. class_lines_pool = populate_sample_lines(num_sample_subclasses) if decoy_class: class_lines_pool.append(non_sample_class_lines) # Subtype fetch is independent of class declaration order, # so validate each permutation. for lines_order in itertools.permutations(class_lines_pool): # Write out class declarations and invoke the function under test. _create_module(lines_by_class=lines_order, filepath=pipe_path) subtype = piface.fetch_sample_subtype( protocol=ATAC_PROTOCOL_NAME, strict_pipe_key=atac_pipe_name, full_pipe_path=pipe_path) # Make the assertion on subtype name, getting additional # information about the module that we defined if there's failure. try: assert exp_subtype_name == subtype.__name__ except AssertionError: with open(pipe_path, 'r') as f: print("PIPELINE MODULE LINES: {}".format("".join( f.readlines()))) raise
def test_problematic_import_builtin_exception(self, tmpdir, error_type, atac_pipe_name, atacseq_piface_data): """ Base Sample is used if builtin exception on pipeline import. """ # Values needed for object creation and function invocation protocol = ATAC_PROTOCOL_NAME protocol_mapping = {protocol: atac_pipe_name} full_pipe_path = os.path.join(tmpdir.strpath, atac_pipe_name) # Modify the data for the ProtocolInterface and create it. atacseq_piface_data[atac_pipe_name][SUBTYPES_KEY] = \ {protocol: "IrrelevantClassname"} conf_path = _write_config_data(protomap=protocol_mapping, conf_data=atacseq_piface_data, dirpath=tmpdir.strpath) piface = ProtocolInterface(conf_path) # We want to test the effect of an encounter with an exception during # the import attempt, so patch the relevant function with a function # to raise the parameterized exception type. with mock.patch("looper.utils.import_from_source", side_effect=error_type()): subtype = piface.fetch_sample_subtype( protocol=protocol, strict_pipe_key=atac_pipe_name, full_pipe_path=full_pipe_path) # When the import hits an exception, the base Sample type is used. assert subtype is Sample
def test_absolute_path(self, atacseq_piface_data, path_config_file, tmpdir, pipe_path, expected_path_base, atac_pipe_name): """ Absolute path regardless of variables works as pipeline path. """ exp_path = os.path.join(tmpdir.strpath, expected_path_base, atac_pipe_name) piface = ProtocolInterface(path_config_file) _, obs_path, _ = piface.finalize_pipeline_key_and_paths(atac_pipe_name) assert exp_path == obs_path
def test_non_dot_relpath_becomes_absolute(self, atacseq_piface_data, path_config_file, tmpdir, pipe_path, atac_pipe_name): """ Relative pipeline path is made absolute when requested by key. """ # TODO: constant-ify "path" and "ATACSeq.py", as well as possibly "pipelines" # and "protocol_mapping" section names of PipelineInterface exp_path = os.path.join(tmpdir.strpath, pipe_path, atac_pipe_name) piface = ProtocolInterface(path_config_file) _, obs_path, _ = piface.finalize_pipeline_key_and_paths(atac_pipe_name) assert exp_path == obs_path
def test_no_path(self, atacseq_piface_data, path_config_file, atac_pipe_name): """ Without explicit path, pipeline is assumed parallel to config. """ piface = ProtocolInterface(path_config_file) # The pipeline is assumed to live alongside its configuration file. config_dirpath = os.path.dirname(path_config_file) expected_pipe_path = os.path.join(config_dirpath, atac_pipe_name) _, full_pipe_path, _ = \ piface.finalize_pipeline_key_and_paths(atac_pipe_name) assert expected_pipe_path == full_pipe_path
def test_warns_about_nonexistent_pipeline_script_path( self, atacseq_piface_data, path_config_file, tmpdir, pipe_path, atac_pipe_name): """ Nonexistent, resolved pipeline script path generates warning. """ name_log_file = "temp-test-log.txt" path_log_file = os.path.join(tmpdir.strpath, name_log_file) temp_hdlr = logging.FileHandler(path_log_file, mode='w') fmt = logging.Formatter(DEV_LOGGING_FMT) temp_hdlr.setFormatter(fmt) temp_hdlr.setLevel(logging.WARN) models._LOGGER.handlers.append(temp_hdlr) pi = ProtocolInterface(path_config_file) pi.finalize_pipeline_key_and_paths(atac_pipe_name) with open(path_log_file, 'r') as logfile: loglines = logfile.readlines() assert 1 == len(loglines) logmsg = loglines[0] assert "WARN" in logmsg and pipe_path in logmsg
def test_relpath_with_dot_becomes_absolute(self, tmpdir, atac_pipe_name, atacseq_piface_data): """ Leading dot drops from relative path, and it's made absolute. """ path_parts = ["relpath", "to", "pipelines", atac_pipe_name] sans_dot_path = os.path.join(*path_parts) pipe_path = os.path.join(".", sans_dot_path) atacseq_piface_data[atac_pipe_name]["path"] = pipe_path exp_path = os.path.join(tmpdir.strpath, sans_dot_path) path_config_file = _write_config_data( protomap={ATAC_PROTOCOL_NAME: atac_pipe_name}, conf_data=atacseq_piface_data, dirpath=tmpdir.strpath) piface = ProtocolInterface(path_config_file) _, obs_path, _ = piface.finalize_pipeline_key_and_paths(atac_pipe_name) # Dot may remain in path, so assert equality of absolute paths. assert os.path.abspath(exp_path) == os.path.abspath(obs_path)
def test_matches_sample_subtype(self, tmpdir, atac_pipe_name, subtype_name, atacseq_piface_data): """ Fetch of subtype is specific even from among multiple subtypes. """ # Basic values pipe_path = os.path.join(tmpdir.strpath, atac_pipe_name) decoy_class = "Decoy" decoy_proto = "DECOY" # Update the ProtocolInterface data and write it out. atacseq_piface_data[atac_pipe_name][SUBTYPES_KEY] = { ATAC_PROTOCOL_NAME: subtype_name, decoy_proto: decoy_class } conf_path = _write_config_data(protomap={ ATAC_PROTOCOL_NAME: atac_pipe_name, decoy_proto: atac_pipe_name }, conf_data=atacseq_piface_data, dirpath=tmpdir.strpath) # Create the collection of definition lines for each class. legit_lines = _class_definition_lines(subtype_name, Sample.__name__) decoy_lines = _class_definition_lines(decoy_class, Sample.__name__) for lines_order in itertools.permutations([legit_lines, decoy_lines]): with open(pipe_path, 'w') as pipe_mod_file: pipe_mod_file.write("{}\n\n".format(SAMPLE_IMPORT)) for class_lines in lines_order: for line in class_lines: pipe_mod_file.write(line) pipe_mod_file.write("\n\n") # We need the new pipeline module file in place before the # ProtocolInterface is created. piface = ProtocolInterface(conf_path) subtype = piface.fetch_sample_subtype( protocol=ATAC_PROTOCOL_NAME, strict_pipe_key=atac_pipe_name, full_pipe_path=pipe_path) assert subtype_name == subtype.__name__
def test_protocol_match_is_fuzzy(self, tmpdir, mapped_protocol, atac_pipe_name, requested_protocol, atacseq_piface_data): """ Punctuation and case mismatches are tolerated in protocol name. """ # Needed to create the ProtocolInterface. protomap = {mapped_protocol: atac_pipe_name} # Needed to invoke the function under test. full_pipe_path = os.path.join(tmpdir.strpath, atac_pipe_name) # PipelineInterface data provided maps name to actual interface data # Mapping, so modify the ATAC-Seq mapping within that. # In this test, we're interested in the resolution of the protocol # name, that with it we can grab the name of a class. Thus, we # need only an arbitrary class name about which we can make the # relevant assertion(s). test_class_name = "TotallyArbitrary" atacseq_piface_data[atac_pipe_name][SUBTYPES_KEY] = \ test_class_name # Write out configuration data and create the ProtocolInterface. conf_path = _write_config_data(protomap=protomap, conf_data=atacseq_piface_data, dirpath=tmpdir.strpath) piface = ProtocolInterface(conf_path) # Make the call under test, patching the function protected # function that's called iff the protocol name match succeeds. with mock.patch("looper.models._import_sample_subtype", return_value=None) as mocked_import: # Return value is irrelevant; the effect of the protocol name # match/resolution is entirely observable via the argument to the # protected import function. piface.fetch_sample_subtype(protocol=requested_protocol, strict_pipe_key=atac_pipe_name, full_pipe_path=full_pipe_path) # When the protocol name match/resolution succeeds, the name of the # Sample subtype class to which it was mapped is passed as an # argument to the protected import function. mocked_import.assert_called_with(full_pipe_path, test_class_name)
def test_pipeline_key_match_is_strict(self, tmpdir, pipe_key, protocol, atac_pipe_name, atacseq_iface_with_resources): """ Request for Sample subtype for unmapped pipeline is KeyError. """ # Create the ProtocolInterface. strict_pipe_key = atac_pipe_name protocol_mapping = {protocol: strict_pipe_key} confpath = _write_config_data( protomap=protocol_mapping, dirpath=tmpdir.strpath, conf_data={strict_pipe_key: atacseq_iface_with_resources}) piface = ProtocolInterface(confpath) # The absolute pipeline path is the pipeline name, joined to the # ProtocolInterface's pipelines location. This location is the # location from which a Sample subtype import is attempted. full_pipe_path = os.path.join(tmpdir.strpath, atac_pipe_name) # TODO: update to pytest.raises(None) if/when 3.1 adoption. # Match between pipeline key specified and the strict key used in # the mapping --> no error while mismatch --> error. if pipe_key == atac_pipe_name: piface.fetch_sample_subtype(protocol, pipe_key, full_pipe_path=full_pipe_path) else: with pytest.raises(KeyError): piface.fetch_sample_subtype(protocol, pipe_key, full_pipe_path=full_pipe_path)
def test_subtype_not_implemented(self, tmpdir, atac_pipe_name, subtype_name, decoy_class, atacseq_piface_data_with_subtypes, subtypes_section_spec_type): """ Subtype that doesn't extend Sample isn't used. """ # Create the pipeline module. pipe_path = os.path.join(tmpdir.strpath, atac_pipe_name) lines = _class_definition_lines("Decoy", "object") \ if decoy_class else [] with open(pipe_path, 'w') as modfile: modfile.write("{}\n\n".format(SAMPLE_IMPORT)) for l in lines: modfile.write(l) conf_path = _write_config_data( protomap={ATAC_PROTOCOL_NAME: atac_pipe_name}, conf_data=atacseq_piface_data_with_subtypes, dirpath=tmpdir.strpath) piface = ProtocolInterface(conf_path) with pytest.raises(ValueError): piface.fetch_sample_subtype(protocol=ATAC_PROTOCOL_NAME, strict_pipe_key=atac_pipe_name, full_pipe_path=pipe_path)
def test_Sample_as_name(self, tmpdir, subtype_name, atac_pipe_name, subtypes_section_spec_type, atacseq_piface_data_with_subtypes): """ A pipeline may redeclare Sample as a subtype name. """ # General values for the test subtype_name = Sample.__name__ pipe_path = os.path.join(tmpdir.strpath, atac_pipe_name) # Define the subtype in the pipeline module. lines = [ "from looper.models import Sample\n", "class {}({}):\n".format(subtype_name, subtype_name), "\tdef __init__(self, *args, **kwargs):\n", "\t\tsuper({}, self).__init__(*args, **kwargs)\n".format( subtype_name) ] with open(pipe_path, 'w') as pipe_module_file: for l in lines: pipe_module_file.write(l) conf_path = _write_config_data( protomap={ATAC_PROTOCOL_NAME: atac_pipe_name}, conf_data=atacseq_piface_data_with_subtypes, dirpath=tmpdir.strpath) piface = ProtocolInterface(conf_path) subtype = piface.fetch_sample_subtype(protocol=ATAC_PROTOCOL_NAME, strict_pipe_key=atac_pipe_name, full_pipe_path=pipe_path) # Establish that subclass relationship is improper. assert issubclass(Sample, Sample) # Our subtype derives from base Sample... assert issubclass(subtype, Sample) # ...but not vice-versa. assert not issubclass(Sample, subtype) # And we retained the name. assert subtype.__name__ == Sample.__name__