editor.py 2.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
  1. from __future__ import annotations
  2. import os
  3. from os.path import exists
  4. from os.path import join
  5. from os.path import splitext
  6. from subprocess import check_call
  7. from typing import Dict
  8. from typing import List
  9. from typing import Mapping
  10. from typing import Optional
  11. from .compat import is_posix
  12. from .exc import CommandError
  13. def open_in_editor(
  14. filename: str, environ: Optional[Dict[str, str]] = None
  15. ) -> None:
  16. """
  17. Opens the given file in a text editor. If the environment variable
  18. ``EDITOR`` is set, this is taken as preference.
  19. Otherwise, a list of commonly installed editors is tried.
  20. If no editor matches, an :py:exc:`OSError` is raised.
  21. :param filename: The filename to open. Will be passed verbatim to the
  22. editor command.
  23. :param environ: An optional drop-in replacement for ``os.environ``. Used
  24. mainly for testing.
  25. """
  26. env = os.environ if environ is None else environ
  27. try:
  28. editor = _find_editor(env)
  29. check_call([editor, filename])
  30. except Exception as exc:
  31. raise CommandError("Error executing editor (%s)" % (exc,)) from exc
  32. def _find_editor(environ: Mapping[str, str]) -> str:
  33. candidates = _default_editors()
  34. for i, var in enumerate(("EDITOR", "VISUAL")):
  35. if var in environ:
  36. user_choice = environ[var]
  37. if exists(user_choice):
  38. return user_choice
  39. if os.sep not in user_choice:
  40. candidates.insert(i, user_choice)
  41. for candidate in candidates:
  42. path = _find_executable(candidate, environ)
  43. if path is not None:
  44. return path
  45. raise OSError(
  46. "No suitable editor found. Please set the "
  47. '"EDITOR" or "VISUAL" environment variables'
  48. )
  49. def _find_executable(
  50. candidate: str, environ: Mapping[str, str]
  51. ) -> Optional[str]:
  52. # Assuming this is on the PATH, we need to determine it's absolute
  53. # location. Otherwise, ``check_call`` will fail
  54. if not is_posix and splitext(candidate)[1] != ".exe":
  55. candidate += ".exe"
  56. for path in environ.get("PATH", "").split(os.pathsep):
  57. value = join(path, candidate)
  58. if exists(value):
  59. return value
  60. return None
  61. def _default_editors() -> List[str]:
  62. # Look for an editor. Prefer the user's choice by env-var, fall back to
  63. # most commonly installed editor (nano/vim)
  64. if is_posix:
  65. return ["sensible-editor", "editor", "nano", "vim", "code"]
  66. else:
  67. return ["code.exe", "notepad++.exe", "notepad.exe"]