| 33 | |
| 34 | |
| 35 | class SourceMapping(object): |
| 36 | def __init__(self, on_source_mapping_changed=NULL): |
| 37 | self._mappings_to_server = {} # dict(normalized(file.py) to [SourceMappingEntry]) |
| 38 | self._mappings_to_client = {} # dict(<cell> to File.py) |
| 39 | self._cache = {} |
| 40 | self._on_source_mapping_changed = on_source_mapping_changed |
| 41 | |
| 42 | def set_source_mapping(self, absolute_filename, mapping): |
| 43 | """ |
| 44 | :param str absolute_filename: |
| 45 | The filename for the source mapping (bytes on py2 and str on py3). |
| 46 | |
| 47 | :param list(SourceMappingEntry) mapping: |
| 48 | A list with the source mapping entries to be applied to the given filename. |
| 49 | |
| 50 | :return str: |
| 51 | An error message if it was not possible to set the mapping or an empty string if |
| 52 | everything is ok. |
| 53 | """ |
| 54 | # Let's first validate if it's ok to apply that mapping. |
| 55 | # File mappings must be 1:N, not M:N (i.e.: if there's a mapping from file1.py to <cell1>, |
| 56 | # there can be no other mapping from any other file to <cell1>). |
| 57 | # This is a limitation to make it easier to remove existing breakpoints when new breakpoints are |
| 58 | # set to a file (so, any file matching that breakpoint can be removed instead of needing to check |
| 59 | # which lines are corresponding to that file). |
| 60 | for map_entry in mapping: |
| 61 | existing_source_filename = self._mappings_to_client.get(map_entry.runtime_source) |
| 62 | if existing_source_filename and existing_source_filename != absolute_filename: |
| 63 | return "Cannot apply mapping from %s to %s (it conflicts with mapping: %s to %s)" % ( |
| 64 | absolute_filename, |
| 65 | map_entry.runtime_source, |
| 66 | existing_source_filename, |
| 67 | map_entry.runtime_source, |
| 68 | ) |
| 69 | |
| 70 | try: |
| 71 | absolute_normalized_filename = pydevd_file_utils.normcase(absolute_filename) |
| 72 | current_mapping = self._mappings_to_server.get(absolute_normalized_filename, []) |
| 73 | for map_entry in current_mapping: |
| 74 | del self._mappings_to_client[map_entry.runtime_source] |
| 75 | |
| 76 | self._mappings_to_server[absolute_normalized_filename] = sorted(mapping, key=lambda entry: entry.line) |
| 77 | |
| 78 | for map_entry in mapping: |
| 79 | self._mappings_to_client[map_entry.runtime_source] = absolute_filename |
| 80 | finally: |
| 81 | self._cache.clear() |
| 82 | self._on_source_mapping_changed() |
| 83 | return "" |
| 84 | |
| 85 | def map_to_client(self, runtime_source_filename, lineno): |
| 86 | key = (lineno, "client", runtime_source_filename) |
| 87 | try: |
| 88 | return self._cache[key] |
| 89 | except KeyError: |
| 90 | for _, mapping in list(self._mappings_to_server.items()): |
| 91 | for map_entry in mapping: |
| 92 | if map_entry.runtime_source == runtime_source_filename: # <cell1> |
no outgoing calls
no test coverage detected