Repository: python-lsp/python-lsp-server
Pull request #480 (2023-11-06 16:40:47)

Rename `_utils` module to `utils`
That's because some functions in that module are used by other packages (e.g. Spyder), so it can't really be private.


Patch:
diff --git a/pylsp/config/config.py b/pylsp/config/config.py
index 454ee4b..cae6b37 100644
--- a/pylsp/config/config.py
+++ b/pylsp/config/config.py
@@ -10,7 +10,7 @@ from typing import List, Mapping, Sequence, Union
 import pluggy
 from pluggy._hooks import HookImpl
 
-from pylsp import _utils, hookspecs, uris, PYLSP
+from pylsp import utils, hookspecs, uris, PYLSP
 
 # See compatibility note on `group` keyword:
 #   https://docs.python.org/3/library/importlib.metadata.html#entry-points
@@ -94,11 +94,11 @@ class Config:
                 log.info("Loaded pylsp plugin %s from %s", name, plugin)
 
         for plugin_conf in self._pm.hook.pylsp_settings(config=self):
-            self._plugin_settings = _utils.merge_dicts(
+            self._plugin_settings = utils.merge_dicts(
                 self._plugin_settings, plugin_conf
             )
 
-        self._plugin_settings = _utils.merge_dicts(
+        self._plugin_settings = utils.merge_dicts(
             self._plugin_settings, self._init_opts.get("pylsp", {})
         )
 
@@ -144,10 +144,10 @@ class Config:
         sources = self._settings.get("configurationSources", DEFAULT_CONFIG_SOURCES)
 
         # Plugin configuration
-        settings = _utils.merge_dicts(settings, self._plugin_settings)
+        settings = utils.merge_dicts(settings, self._plugin_settings)
 
         # LSP configuration
-        settings = _utils.merge_dicts(settings, self._settings)
+        settings = utils.merge_dicts(settings, self._settings)
 
         # User configuration
         for source_name in reversed(sources):
@@ -158,7 +158,7 @@ class Config:
             log.debug(
                 "Got user config from %s: %s", source.__class__.__name__, source_conf
             )
-            settings = _utils.merge_dicts(settings, source_conf)
+            settings = utils.merge_dicts(settings, source_conf)
 
         # Project configuration
         for source_name in reversed(sources):
@@ -169,7 +169,7 @@ class Config:
             log.debug(
                 "Got project config from %s: %s", source.__class__.__name__, source_conf
             )
-            settings = _utils.merge_dicts(settings, source_conf)
+            settings = utils.merge_dicts(settings, source_conf)
 
         log.debug("With configuration: %s", settings)
 
@@ -177,7 +177,7 @@ class Config:
 
     def find_parents(self, path, names):
         root_path = uris.to_fs_path(self._root_uri)
-        return _utils.find_parents(root_path, path, names)
+        return utils.find_parents(root_path, path, names)
 
     def plugin_settings(self, plugin, document_path=None):
         return (
diff --git a/pylsp/config/flake8_conf.py b/pylsp/config/flake8_conf.py
index ca3b199..4514eb5 100644
--- a/pylsp/config/flake8_conf.py
+++ b/pylsp/config/flake8_conf.py
@@ -3,7 +3,7 @@
 
 import logging
 import os
-from pylsp._utils import find_parents
+from pylsp.utils import find_parents
 from .source import ConfigSource
 
 log = logging.getLogger(__name__)
diff --git a/pylsp/config/pycodestyle_conf.py b/pylsp/config/pycodestyle_conf.py
index 98d8a1b..c324ec4 100644
--- a/pylsp/config/pycodestyle_conf.py
+++ b/pylsp/config/pycodestyle_conf.py
@@ -2,7 +2,7 @@
 # Copyright 2021- Python Language Server Contributors.
 
 import pycodestyle
-from pylsp._utils import find_parents
+from pylsp.utils import find_parents
 from .source import ConfigSource
 
 
diff --git a/pylsp/plugins/autopep8_format.py b/pylsp/plugins/autopep8_format.py
index 1ae3e5f..9710a86 100644
--- a/pylsp/plugins/autopep8_format.py
+++ b/pylsp/plugins/autopep8_format.py
@@ -7,7 +7,7 @@ import pycodestyle
 from autopep8 import fix_code, continued_indentation as autopep8_c_i
 
 from pylsp import hookimpl
-from pylsp._utils import get_eol_chars
+from pylsp.utils import get_eol_chars
 
 log = logging.getLogger(__name__)
 
diff --git a/pylsp/plugins/definition.py b/pylsp/plugins/definition.py
index 53eda91..73d156a 100644
--- a/pylsp/plugins/definition.py
+++ b/pylsp/plugins/definition.py
@@ -6,7 +6,7 @@ from typing import Any, Dict, List, TYPE_CHECKING
 
 import jedi
 
-from pylsp import hookimpl, uris, _utils
+from pylsp import hookimpl, uris, utils
 
 if TYPE_CHECKING:
     from jedi.api import Script
@@ -44,7 +44,7 @@ def pylsp_definitions(
     config: Config, document: Document, position: Dict[str, int]
 ) -> List[Dict[str, Any]]:
     settings = config.plugin_settings("jedi_definition")
-    code_position = _utils.position_to_jedi_linecolumn(document, position)
+    code_position = utils.position_to_jedi_linecolumn(document, position)
     script = document.jedi_script(use_document_path=True)
     auto_import_modules = jedi.settings.auto_import_modules
 
diff --git a/pylsp/plugins/highlight.py b/pylsp/plugins/highlight.py
index 0dd896c..0ed040e 100644
--- a/pylsp/plugins/highlight.py
+++ b/pylsp/plugins/highlight.py
@@ -2,14 +2,14 @@
 # Copyright 2021- Python Language Server Contributors.
 
 import logging
-from pylsp import hookimpl, lsp, _utils
+from pylsp import hookimpl, lsp, utils
 
 log = logging.getLogger(__name__)
 
 
 @hookimpl
 def pylsp_document_highlight(document, position):
-    code_position = _utils.position_to_jedi_linecolumn(document, position)
+    code_position = utils.position_to_jedi_linecolumn(document, position)
     usages = document.jedi_script().get_references(**code_position)
 
     def is_valid(definition):
diff --git a/pylsp/plugins/hover.py b/pylsp/plugins/hover.py
index ae07b3d..cccfdfb 100644
--- a/pylsp/plugins/hover.py
+++ b/pylsp/plugins/hover.py
@@ -3,14 +3,14 @@
 
 import logging
 
-from pylsp import hookimpl, _utils
+from pylsp import hookimpl, utils
 
 log = logging.getLogger(__name__)
 
 
 @hookimpl
 def pylsp_hover(config, document, position):
-    code_position = _utils.position_to_jedi_linecolumn(document, position)
+    code_position = utils.position_to_jedi_linecolumn(document, position)
     definitions = document.jedi_script(use_document_path=True).infer(**code_position)
     word = document.word_at_position(position)
 
@@ -28,7 +28,7 @@ def pylsp_hover(config, document, position):
 
     hover_capabilities = config.capabilities.get("textDocument", {}).get("hover", {})
     supported_markup_kinds = hover_capabilities.get("contentFormat", ["markdown"])
-    preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds)
+    preferred_markup_kind = utils.choose_markup_kind(supported_markup_kinds)
 
     # Find first exact matching signature
     signature = next(
@@ -41,7 +41,7 @@ def pylsp_hover(config, document, position):
     )
 
     return {
-        "contents": _utils.format_docstring(
+        "contents": utils.format_docstring(
             # raw docstring returns only doc, without signature
             definition.docstring(raw=True),
             preferred_markup_kind,
diff --git a/pylsp/plugins/jedi_completion.py b/pylsp/plugins/jedi_completion.py
index 2ecf0be..324a127 100644
--- a/pylsp/plugins/jedi_completion.py
+++ b/pylsp/plugins/jedi_completion.py
@@ -6,7 +6,7 @@ import os
 
 import parso
 
-from pylsp import _utils, hookimpl, lsp
+from pylsp import utils, hookimpl, lsp
 from pylsp.plugins._resolvers import LABEL_RESOLVER, SNIPPET_RESOLVER
 
 log = logging.getLogger(__name__)
@@ -41,7 +41,7 @@ def pylsp_completions(config, document, position):
     # pylint: disable=too-many-locals
     settings = config.plugin_settings("jedi_completion", document_path=document.path)
     resolve_eagerly = settings.get("eager", False)
-    code_position = _utils.position_to_jedi_linecolumn(document, position)
+    code_position = utils.position_to_jedi_linecolumn(document, position)
 
     code_position["fuzzy"] = settings.get("fuzzy", False)
     completions = document.jedi_script(use_document_path=True).complete(**code_position)
@@ -55,7 +55,7 @@ def pylsp_completions(config, document, position):
     item_capabilities = completion_capabilities.get("completionItem", {})
     snippet_support = item_capabilities.get("snippetSupport")
     supported_markup_kinds = item_capabilities.get("documentationFormat", ["markdown"])
-    preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds)
+    preferred_markup_kind = utils.choose_markup_kind(supported_markup_kinds)
 
     should_include_params = settings.get("include_params")
     should_include_class_objects = settings.get("include_class_objects", False)
@@ -146,7 +146,7 @@ def pylsp_completion_item_resolve(config, completion_item, document):
     )
     item_capabilities = completion_capabilities.get("completionItem", {})
     supported_markup_kinds = item_capabilities.get("documentationFormat", ["markdown"])
-    preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds)
+    preferred_markup_kind = utils.choose_markup_kind(supported_markup_kinds)
 
     if shared_data:
         completion, data = shared_data
@@ -209,7 +209,7 @@ def _resolve_completion(completion, d, markup_kind: str):
     # pylint: disable=broad-except
     completion["detail"] = _detail(d)
     try:
-        docs = _utils.format_docstring(
+        docs = utils.format_docstring(
             d.docstring(raw=True),
             signatures=[signature.to_string() for signature in d.get_signatures()],
             markup_kind=markup_kind,
diff --git a/pylsp/plugins/jedi_rename.py b/pylsp/plugins/jedi_rename.py
index 9c89c1d..65ae759 100644
--- a/pylsp/plugins/jedi_rename.py
+++ b/pylsp/plugins/jedi_rename.py
@@ -3,7 +3,7 @@
 
 import logging
 
-from pylsp import hookimpl, uris, _utils
+from pylsp import hookimpl, uris, utils
 
 log = logging.getLogger(__name__)
 
@@ -15,7 +15,7 @@ def pylsp_rename(
     log.debug(
         "Executing rename of %s to %s", document.word_at_position(position), new_name
     )
-    kwargs = _utils.position_to_jedi_linecolumn(document, position)
+    kwargs = utils.position_to_jedi_linecolumn(document, position)
     kwargs["new_name"] = new_name
     try:
         refactoring = document.jedi_script().rename(**kwargs)
@@ -54,6 +54,6 @@ def pylsp_rename(
 
 def _num_lines(file_contents):
     "Count the number of lines in the given string."
-    if _utils.get_eol_chars(file_contents):
+    if utils.get_eol_chars(file_contents):
         return len(file_contents.splitlines())
     return 0
diff --git a/pylsp/plugins/pycodestyle_lint.py b/pylsp/plugins/pycodestyle_lint.py
index 62b0b8a..83dc7dd 100644
--- a/pylsp/plugins/pycodestyle_lint.py
+++ b/pylsp/plugins/pycodestyle_lint.py
@@ -6,7 +6,7 @@ import logging
 import pycodestyle
 
 from pylsp import hookimpl, lsp
-from pylsp._utils import get_eol_chars
+from pylsp.utils import get_eol_chars
 
 try:
     from autopep8 import continued_indentation as autopep8_c_i
diff --git a/pylsp/plugins/references.py b/pylsp/plugins/references.py
index fadf1de..b05c76b 100644
--- a/pylsp/plugins/references.py
+++ b/pylsp/plugins/references.py
@@ -2,14 +2,14 @@
 # Copyright 2021- Python Language Server Contributors.
 
 import logging
-from pylsp import hookimpl, uris, _utils
+from pylsp import hookimpl, uris, utils
 
 log = logging.getLogger(__name__)
 
 
 @hookimpl
 def pylsp_references(document, position, exclude_declaration):
-    code_position = _utils.position_to_jedi_linecolumn(document, position)
+    code_position = utils.position_to_jedi_linecolumn(document, position)
     usages = document.jedi_script().get_references(**code_position)
 
     if exclude_declaration:
diff --git a/pylsp/plugins/rope_completion.py b/pylsp/plugins/rope_completion.py
index ca0d434..b30458c 100644
--- a/pylsp/plugins/rope_completion.py
+++ b/pylsp/plugins/rope_completion.py
@@ -4,7 +4,7 @@
 import logging
 from rope.contrib.codeassist import code_assist, sorted_proposals
 
-from pylsp import _utils, hookimpl, lsp
+from pylsp import utils, hookimpl, lsp
 
 
 log = logging.getLogger(__name__)
@@ -19,7 +19,7 @@ def pylsp_settings():
 def _resolve_completion(completion, data, markup_kind):
     # pylint: disable=broad-except
     try:
-        doc = _utils.format_docstring(data.get_doc(), markup_kind=markup_kind)
+        doc = utils.format_docstring(data.get_doc(), markup_kind=markup_kind)
     except Exception as e:
         log.debug("Failed to resolve Rope completion: %s", e)
         doc = ""
@@ -57,7 +57,7 @@ def pylsp_completions(config, workspace, document, position):
     )
     item_capabilities = completion_capabilities.get("completionItem", {})
     supported_markup_kinds = item_capabilities.get("documentationFormat", ["markdown"])
-    preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds)
+    preferred_markup_kind = utils.choose_markup_kind(supported_markup_kinds)
 
     try:
         definitions = code_assist(
@@ -104,7 +104,7 @@ def pylsp_completion_item_resolve(config, completion_item, document):
     )
     item_capabilities = completion_capabilities.get("completionItem", {})
     supported_markup_kinds = item_capabilities.get("documentationFormat", ["markdown"])
-    preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds)
+    preferred_markup_kind = utils.choose_markup_kind(supported_markup_kinds)
 
     if shared_data:
         completion, data = shared_data
diff --git a/pylsp/plugins/rope_rename.py b/pylsp/plugins/rope_rename.py
index a4323a4..d0873e6 100644
--- a/pylsp/plugins/rope_rename.py
+++ b/pylsp/plugins/rope_rename.py
@@ -6,7 +6,7 @@ import logging
 from rope.base import libutils
 from rope.refactor.rename import Rename
 
-from pylsp import hookimpl, uris, _utils
+from pylsp import hookimpl, uris, utils
 
 log = logging.getLogger(__name__)
 
@@ -61,6 +61,6 @@ def _num_lines(resource):
     "Count the number of lines in a `File` resource."
     text = resource.read()
 
-    if _utils.get_eol_chars(text):
+    if utils.get_eol_chars(text):
         return len(text.splitlines())
     return 0
diff --git a/pylsp/plugins/signature.py b/pylsp/plugins/signature.py
index 4fc93df..0e625b2 100644
--- a/pylsp/plugins/signature.py
+++ b/pylsp/plugins/signature.py
@@ -3,7 +3,7 @@
 
 import logging
 import re
-from pylsp import hookimpl, _utils
+from pylsp import hookimpl, utils
 
 log = logging.getLogger(__name__)
 
@@ -16,7 +16,7 @@ DOC_REGEX = [SPHINX, EPYDOC, GOOGLE]
 
 @hookimpl
 def pylsp_signature_help(config, document, position):
-    code_position = _utils.position_to_jedi_linecolumn(document, position)
+    code_position = utils.position_to_jedi_linecolumn(document, position)
     signatures = document.jedi_script().get_signatures(**code_position)
 
     if not signatures:
@@ -31,7 +31,7 @@ def pylsp_signature_help(config, document, position):
     supported_markup_kinds = signature_information_support.get(
         "documentationFormat", ["markdown"]
     )
-    preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds)
+    preferred_markup_kind = utils.choose_markup_kind(supported_markup_kinds)
 
     s = signatures[0]
 
@@ -42,7 +42,7 @@ def pylsp_signature_help(config, document, position):
     function_sig = " ".join([line.strip() for line in function_sig_lines])
     sig = {
         "label": function_sig,
-        "documentation": _utils.format_docstring(
+        "documentation": utils.format_docstring(
             s.docstring(raw=True), markup_kind=preferred_markup_kind
         ),
     }
@@ -52,7 +52,7 @@ def pylsp_signature_help(config, document, position):
         sig["parameters"] = [
             {
                 "label": p.name,
-                "documentation": _utils.format_docstring(
+                "documentation": utils.format_docstring(
                     _param_docs(docstring, p.name), markup_kind=preferred_markup_kind
                 ),
             }
diff --git a/pylsp/plugins/yapf_format.py b/pylsp/plugins/yapf_format.py
index 4a8111b..78a0a79 100644
--- a/pylsp/plugins/yapf_format.py
+++ b/pylsp/plugins/yapf_format.py
@@ -10,7 +10,7 @@ from yapf.yapflib.yapf_api import FormatCode
 import whatthepatch
 
 from pylsp import hookimpl
-from pylsp._utils import get_eol_chars
+from pylsp.utils import get_eol_chars
 
 log = logging.getLogger(__name__)
 
diff --git a/pylsp/python_lsp.py b/pylsp/python_lsp.py
index 52a22a3..25e3126 100644
--- a/pylsp/python_lsp.py
+++ b/pylsp/python_lsp.py
@@ -14,7 +14,7 @@ from pylsp_jsonrpc.dispatchers import MethodDispatcher
 from pylsp_jsonrpc.endpoint import Endpoint
 from pylsp_jsonrpc.streams import JsonRpcStreamReader, JsonRpcStreamWriter
 
-from . import lsp, _utils, uris
+from . import lsp, utils, uris
 from .config import config
 from .workspace import Workspace, Document, Notebook, Cell
 from ._version import __version__
@@ -250,7 +250,7 @@ class PythonLSPServer(MethodDispatcher):
             self._jsonrpc_stream_writer.close()
 
     def _match_uri_to_workspace(self, uri):
-        workspace_uri = _utils.match_uri_to_workspace(uri, self.workspaces)
+        workspace_uri = utils.match_uri_to_workspace(uri, self.workspaces)
         return self.workspaces.get(workspace_uri, self.workspace)
 
     def _hook(self, hook_name, doc_uri=None, **kwargs):
@@ -360,7 +360,7 @@ class PythonLSPServer(MethodDispatcher):
 
             def watch_parent_process(pid):
                 # exit when the given pid is not alive
-                if not _utils.is_process_alive(pid):
+                if not utils.is_process_alive(pid):
                     log.info("parent process %s is not alive, exiting!", pid)
                     self.m_exit()
                 else:
@@ -439,7 +439,7 @@ class PythonLSPServer(MethodDispatcher):
     def hover(self, doc_uri, position):
         return self._hook("pylsp_hover", doc_uri, position=position) or {"contents": ""}
 
-    @_utils.debounce(LINT_DEBOUNCE_S, keyed_by="doc_uri")
+    @utils.debounce(LINT_DEBOUNCE_S, keyed_by="doc_uri")
     def lint(self, doc_uri, is_saved):
         # Since we're debounced, the document may no longer be open
         workspace = self._match_uri_to_workspace(doc_uri)
diff --git a/pylsp/_utils.py b/pylsp/utils.py
similarity index 100%
rename from pylsp/_utils.py
rename to pylsp/utils.py
diff --git a/pylsp/workspace.py b/pylsp/workspace.py
index 5c6880c..e87b030 100644
--- a/pylsp/workspace.py
+++ b/pylsp/workspace.py
@@ -13,7 +13,7 @@ from threading import RLock
 
 import jedi
 
-from . import lsp, uris, _utils
+from . import lsp, uris, utils
 
 log = logging.getLogger(__name__)
 
@@ -339,7 +339,7 @@ class Workspace:
     def source_roots(self, document_path):
         """Return the source roots for the given document."""
         files = (
-            _utils.find_parents(
+            utils.find_parents(
                 self._root_path, document_path, ["setup.py", "pyproject.toml"]
             )
             or []
@@ -406,7 +406,7 @@ class Document:
         self.uri = uri
         self.version = version
         self.path = uris.to_fs_path(uri)
-        self.dot_path = _utils.path_to_dot_name(self.path)
+        self.dot_path = utils.path_to_dot_name(self.path)
         self.filename = os.path.basename(self.path)
         self.shared_data = {}
 
@@ -567,7 +567,7 @@ class Document:
 
         if position:
             # Deprecated by Jedi to use in Script() constructor
-            kwargs += _utils.position_to_jedi_linecolumn(self, position)
+            kwargs += utils.position_to_jedi_linecolumn(self, position)
 
         return jedi.Script(**kwargs)
 


Test patch:
diff --git a/test/plugins/test_completion.py b/test/plugins/test_completion.py
index 00a54eb..6ccff82 100644
--- a/test/plugins/test_completion.py
+++ b/test/plugins/test_completion.py
@@ -17,7 +17,7 @@ from pylsp.plugins.jedi_completion import (
     pylsp_completion_item_resolve as pylsp_jedi_completion_item_resolve,
 )
 from pylsp.plugins.rope_completion import pylsp_completions as pylsp_rope_completions
-from pylsp._utils import JEDI_VERSION
+from pylsp.utils import JEDI_VERSION
 
 
 PY2 = sys.version[0] == "2"
diff --git a/test/test_utils.py b/test/test_utils.py
index 8b518d7..b77f1f4 100644
--- a/test/test_utils.py
+++ b/test/test_utils.py
@@ -12,7 +12,7 @@ from unittest import mock
 from flaky import flaky
 from docstring_to_markdown import UnknownFormatError
 
-from pylsp import _utils
+from pylsp import utils
 from pylsp.lsp import NotebookCellKind
 from pylsp.python_lsp import PythonLSPServer, start_io_lang_server
 
@@ -128,7 +128,7 @@ def test_debounce():
     interval = 0.1
     obj = mock.Mock()
 
-    @_utils.debounce(0.1)
+    @utils.debounce(0.1)
     def call_m():
         obj()
 
@@ -152,7 +152,7 @@ def test_debounce_keyed_by():
     interval = 0.1
     obj = mock.Mock()
 
-    @_utils.debounce(0.1, keyed_by="key")
+    @utils.debounce(0.1, keyed_by="key")
     def call_m(key):
         obj(key)
 
@@ -182,8 +182,8 @@ def test_debounce_keyed_by():
 
 
 def test_list_to_string():
-    assert _utils.list_to_string("string") == "string"
-    assert _utils.list_to_string(["a", "r", "r", "a", "y"]) == "a,r,r,a,y"
+    assert utils.list_to_string("string") == "string"
+    assert utils.list_to_string(["a", "r", "r", "a", "y"]) == "a,r,r,a,y"
 
 
 def test_find_parents(tmpdir):
@@ -191,28 +191,28 @@ def test_find_parents(tmpdir):
     path = subsubdir.ensure("path.py")
     test_cfg = tmpdir.ensure("test.cfg")
 
-    assert _utils.find_parents(tmpdir.strpath, path.strpath, ["test.cfg"]) == [
+    assert utils.find_parents(tmpdir.strpath, path.strpath, ["test.cfg"]) == [
         test_cfg.strpath
     ]
 
 
 def test_merge_dicts():
-    assert _utils.merge_dicts(
+    assert utils.merge_dicts(
         {"a": True, "b": {"x": 123, "y": {"hello": "world"}}},
         {"a": False, "b": {"y": [], "z": 987}},
     ) == {"a": False, "b": {"x": 123, "y": [], "z": 987}}
 
 
 def test_clip_column():
-    assert _utils.clip_column(0, [], 0) == 0
-    assert _utils.clip_column(2, ["123"], 0) == 2
-    assert _utils.clip_column(3, ["123"], 0) == 3
-    assert _utils.clip_column(5, ["123"], 0) == 3
-    assert _utils.clip_column(0, ["\n", "123"], 0) == 0
-    assert _utils.clip_column(1, ["\n", "123"], 0) == 0
-    assert _utils.clip_column(2, ["123\n", "123"], 0) == 2
-    assert _utils.clip_column(3, ["123\n", "123"], 0) == 3
-    assert _utils.clip_column(4, ["123\n", "123"], 1) == 3
+    assert utils.clip_column(0, [], 0) == 0
+    assert utils.clip_column(2, ["123"], 0) == 2
+    assert utils.clip_column(3, ["123"], 0) == 3
+    assert utils.clip_column(5, ["123"], 0) == 3
+    assert utils.clip_column(0, ["\n", "123"], 0) == 0
+    assert utils.clip_column(1, ["\n", "123"], 0) == 0
+    assert utils.clip_column(2, ["123\n", "123"], 0) == 2
+    assert utils.clip_column(3, ["123\n", "123"], 0) == 3
+    assert utils.clip_column(4, ["123\n", "123"], 1) == 3
 
 
 @mock.patch("docstring_to_markdown.convert")
@@ -233,14 +233,14 @@ def test_format_docstring_valid_rst_signature(mock_convert):
     - `a`: str, something
     """
 
-    markdown = _utils.format_docstring(
+    markdown = utils.format_docstring(
         docstring,
         "markdown",
         ["something(a: str) -> str"],
     )["value"]
 
     assert markdown.startswith(
-        _utils.wrap_signature("something(a: str) -> str"),
+        utils.wrap_signature("something(a: str) -> str"),
     )
 
 
@@ -254,12 +254,12 @@ def test_format_docstring_invalid_rst_signature(_):
     a : str, something
     """
 
-    markdown = _utils.format_docstring(
+    markdown = utils.format_docstring(
         docstring,
         "markdown",
         ["something(a: str) -> str"],
     )["value"]
 
     assert markdown.startswith(
-        _utils.wrap_signature("something(a: str) -> str"),
+        utils.wrap_signature("something(a: str) -> str"),
     )